mirror of
https://github.com/SigNoz/signoz.git
synced 2026-09-01 00:50:42 +01:00
Compare commits
20 Commits
issue_5975
...
feat/licen
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f28d42366 | ||
|
|
b67fb40ae7 | ||
|
|
31b65283f3 | ||
|
|
f08fc63533 | ||
|
|
39c69c4791 | ||
|
|
c62baa97b5 | ||
|
|
7c0dce31ed | ||
|
|
f65c27c236 | ||
|
|
405e92cd06 | ||
|
|
ce3966b8c4 | ||
|
|
f2ce56de35 | ||
|
|
5e06339afe | ||
|
|
32f9254035 | ||
|
|
764fe8ec69 | ||
|
|
ea7fb4cdaa | ||
|
|
da9b4644df | ||
|
|
095821264e | ||
|
|
5069bf80b0 | ||
|
|
1086914a3b | ||
|
|
3e2daaea0e |
@@ -96,6 +96,7 @@ 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,
|
||||
|
||||
@@ -2559,6 +2559,7 @@ components:
|
||||
- factor-api-key
|
||||
- license
|
||||
- subscription
|
||||
- deployment-host
|
||||
- logs
|
||||
- traces
|
||||
- metrics
|
||||
@@ -5641,6 +5642,219 @@ 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:
|
||||
@@ -23942,9 +24156,9 @@ paths:
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- VIEWER
|
||||
- deployment-host:list
|
||||
- tokenizer:
|
||||
- VIEWER
|
||||
- deployment-host:list
|
||||
summary: Get host info from Zeus.
|
||||
tags:
|
||||
- zeus
|
||||
@@ -23998,9 +24212,9 @@ paths:
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- ADMIN
|
||||
- deployment-host:update
|
||||
- tokenizer:
|
||||
- ADMIN
|
||||
- deployment-host:update
|
||||
summary: Put host in Zeus for a deployment.
|
||||
tags:
|
||||
- zeus
|
||||
@@ -24199,6 +24413,347 @@ 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
|
||||
|
||||
@@ -22,89 +22,6 @@ 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()
|
||||
|
||||
@@ -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 {
|
||||
data, err := provider.zeus.GetLicense(ctx, key)
|
||||
zeusLicense, err := provider.zeus.GetLicense(ctx, key)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "unable to fetch license data with upstream server")
|
||||
}
|
||||
|
||||
license, err := licensetypes.NewLicense(data, organizationID)
|
||||
license, err := licensetypes.NewLicense(zeusLicense, organizationID)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to create license entity")
|
||||
}
|
||||
@@ -115,6 +115,47 @@ 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 {
|
||||
@@ -139,7 +180,7 @@ func (provider *provider) Refresh(ctx context.Context, organizationID valuer.UUI
|
||||
return err
|
||||
}
|
||||
|
||||
data, err := provider.zeus.GetLicense(ctx, activeLicense.Key)
|
||||
zeusLicense, 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)
|
||||
@@ -154,7 +195,7 @@ func (provider *provider) Refresh(ctx context.Context, organizationID valuer.UUI
|
||||
return err
|
||||
}
|
||||
|
||||
err = activeLicense.Update(data)
|
||||
err = activeLicense.Update(zeusLicense)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to create license entity from license data")
|
||||
}
|
||||
|
||||
@@ -64,6 +64,22 @@ 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.
|
||||
|
||||
@@ -76,11 +76,6 @@ 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)
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ func New(ctx context.Context, providerSettings factory.ProviderSettings, config
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (provider *Provider) GetLicense(ctx context.Context, key string) ([]byte, error) {
|
||||
func (provider *Provider) GetLicense(ctx context.Context, key string) (*zeustypes.License, error) {
|
||||
response, err := provider.do(
|
||||
ctx,
|
||||
provider.config.URL.JoinPath("/v2/licenses/me"),
|
||||
@@ -63,7 +63,12 @@ func (provider *Provider) GetLicense(ctx context.Context, key string) ([]byte, e
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return []byte(gjson.GetBytes(response, "data").String()), nil
|
||||
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
|
||||
}
|
||||
|
||||
func (provider *Provider) GetCheckoutURL(ctx context.Context, key string, body []byte) ([]byte, error) {
|
||||
|
||||
@@ -103,30 +103,30 @@ function createMockLicense(
|
||||
overrides: Partial<LicenseResModel> = {},
|
||||
): LicenseResModel {
|
||||
return {
|
||||
key: 'test-key',
|
||||
event_queue: {
|
||||
created_at: '0',
|
||||
id: 'test-license-id',
|
||||
eventQueue: {
|
||||
createdAt: '0',
|
||||
event: LicenseEvent.NO_EVENT,
|
||||
scheduled_at: '0',
|
||||
scheduledAt: '0',
|
||||
status: '',
|
||||
updated_at: '0',
|
||||
updatedAt: '0',
|
||||
},
|
||||
state: LicenseState.ACTIVATED,
|
||||
status: LicenseStatus.VALID,
|
||||
platform: LicensePlatform.CLOUD,
|
||||
created_at: '0',
|
||||
createdAt: '0',
|
||||
plan: {
|
||||
created_at: '0',
|
||||
id: '0',
|
||||
createdAt: '0',
|
||||
description: '',
|
||||
is_active: true,
|
||||
isActive: true,
|
||||
name: '',
|
||||
updated_at: '0',
|
||||
updatedAt: '0',
|
||||
},
|
||||
plan_id: '0',
|
||||
free_until: '0',
|
||||
updated_at: '0',
|
||||
valid_from: 0,
|
||||
valid_until: 0,
|
||||
freeUntil: '0',
|
||||
updatedAt: '0',
|
||||
validFrom: 0,
|
||||
validUntil: 0,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
538
frontend/src/api/generated/services/licenses/index.ts
Normal file
538
frontend/src/api/generated/services/licenses/index.ts
Normal file
@@ -0,0 +1,538 @@
|
||||
/**
|
||||
* ! 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;
|
||||
};
|
||||
@@ -2175,6 +2175,7 @@ export enum CoretypesKindDTO {
|
||||
'factor-api-key' = 'factor-api-key',
|
||||
license = 'license',
|
||||
subscription = 'subscription',
|
||||
'deployment-host' = 'deployment-host',
|
||||
logs = 'logs',
|
||||
traces = 'traces',
|
||||
metrics = 'metrics',
|
||||
@@ -7172,6 +7173,247 @@ 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
|
||||
*/
|
||||
@@ -12428,6 +12670,42 @@ 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;
|
||||
};
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
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;
|
||||
@@ -1,24 +0,0 @@
|
||||
import { ApiV3Instance as axios } from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/licenses/apply';
|
||||
|
||||
const apply = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponseV2<PayloadProps>> => {
|
||||
try {
|
||||
const response = await axios.post<PayloadProps>('/licenses', {
|
||||
key: props.key,
|
||||
});
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
}
|
||||
};
|
||||
|
||||
export default apply;
|
||||
@@ -1,20 +0,0 @@
|
||||
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;
|
||||
@@ -7,9 +7,8 @@ import axios from 'axios';
|
||||
import TextToolTip from 'components/TextToolTip';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { useOptionsMenu } from 'container/OptionsMenu';
|
||||
import { useGetSearchQueryParam } from 'hooks/queryBuilder/useGetSearchQueryParam';
|
||||
import { useGetSavedViewParams } from 'hooks/saveViews/useGetSavedViewParams';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useDeleteView } from 'hooks/saveViews/useDeleteView';
|
||||
import { useGetAllViews } from 'hooks/saveViews/useGetAllViews';
|
||||
@@ -69,9 +68,7 @@ function ExplorerCard({
|
||||
setIsOpen(newOpen);
|
||||
};
|
||||
|
||||
const viewName = useGetSearchQueryParam(QueryParams.viewName) || '';
|
||||
|
||||
const viewKey = useGetSearchQueryParam(QueryParams.viewKey) || '';
|
||||
const { viewName, viewKey } = useGetSavedViewParams();
|
||||
|
||||
const { options } = useOptionsMenu({
|
||||
storageKey:
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { QueryParams } from 'constants/query';
|
||||
|
||||
export const ExploreHeaderToolTip = {
|
||||
url: 'https://signoz.io/docs/querying/overview/?utm_source=product&utm_medium=new-query-builder',
|
||||
text: 'More details on how to use query builder',
|
||||
@@ -9,5 +7,3 @@ export const SaveButtonText = {
|
||||
SAVE_AS_NEW_VIEW: 'Save as new view',
|
||||
SAVE_VIEW: 'Save view',
|
||||
};
|
||||
|
||||
export type QuerySearchParamNames = QueryParams.viewName | QueryParams.viewKey;
|
||||
|
||||
@@ -241,28 +241,29 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
||||
))
|
||||
)}
|
||||
|
||||
{!showOnlyWhereClause && currentQuery.builder.queryFormulas.length > 0 && (
|
||||
<div className="qb-formulas-container">
|
||||
{currentQuery.builder.queryFormulas.map((formula, index) => {
|
||||
const query =
|
||||
currentQuery.builder.queryData[index] ||
|
||||
currentQuery.builder.queryData[0];
|
||||
{!showOnlyWhereClause &&
|
||||
currentQuery.builder.queryFormulas?.length > 0 && (
|
||||
<div className="qb-formulas-container">
|
||||
{currentQuery.builder.queryFormulas.map((formula, index) => {
|
||||
const query =
|
||||
currentQuery.builder.queryData[index] ||
|
||||
currentQuery.builder.queryData[0];
|
||||
|
||||
return (
|
||||
<div key={formula.queryName} className="qb-formula">
|
||||
<Formula
|
||||
filterConfigs={filterConfigs}
|
||||
query={query}
|
||||
formula={formula}
|
||||
index={index}
|
||||
isAdditionalFilterEnable={false}
|
||||
isQBV2
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
return (
|
||||
<div key={formula.queryName} className="qb-formula">
|
||||
<Formula
|
||||
filterConfigs={filterConfigs}
|
||||
query={query}
|
||||
formula={formula}
|
||||
index={index}
|
||||
isAdditionalFilterEnable={false}
|
||||
isQBV2
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{shouldShowFooter && (
|
||||
<QueryFooter
|
||||
@@ -290,7 +291,7 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
||||
</div>
|
||||
))}
|
||||
|
||||
{currentQuery.builder.queryFormulas.map((formula) => (
|
||||
{currentQuery.builder.queryFormulas?.map((formula) => (
|
||||
<div key={formula.queryName} className="formula-name">
|
||||
{formula.queryName}
|
||||
</div>
|
||||
|
||||
@@ -212,6 +212,32 @@ describe('QueryBuilderV2 + QueryV2 - base render', () => {
|
||||
expect(handleRunQueryMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not crash when builder.queryFormulas/queryTraceOperator are missing (partial/legacy query)', () => {
|
||||
const currentQueryBase = baseQBContext.currentQuery as Query;
|
||||
|
||||
mockedUseQueryBuilder.mockReturnValue({
|
||||
...baseQBContext,
|
||||
currentQuery: {
|
||||
...currentQueryBase,
|
||||
builder: {
|
||||
queryData: currentQueryBase.builder.queryData,
|
||||
queryFormulas: undefined as unknown as [],
|
||||
queryTraceOperator: undefined as unknown as [],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(() =>
|
||||
render(<QueryBuilderV2 panelType={PANEL_TYPES.TABLE} version="v4" />),
|
||||
).not.toThrow();
|
||||
|
||||
// query list still renders from queryData, formulas block is skipped
|
||||
expect(document.querySelector('.query-names-section')).toBeInTheDocument();
|
||||
expect(
|
||||
document.querySelector('.qb-formulas-container'),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('fx button is disabled when functions already exist', () => {
|
||||
const currentQueryBase = baseQBContext.currentQuery as Query;
|
||||
const supersetQueryBase = baseQBContext.supersetQuery as Query;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import refreshPaymentStatus from 'api/v3/licenses/put';
|
||||
import { refreshLicense } from 'api/generated/services/licenses';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
import { RefreshCcw } from '@signozhq/icons';
|
||||
@@ -14,17 +14,21 @@ function RefreshPaymentStatus({
|
||||
className?: string;
|
||||
}): JSX.Element {
|
||||
const { t } = useTranslation(['failedPayment']);
|
||||
const { activeLicenseRefetch } = useAppContext();
|
||||
const { activeLicense, activeLicenseRefetch } = useAppContext();
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handleRefreshPaymentStatus = async (): Promise<void> => {
|
||||
if (!activeLicense) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
await refreshPaymentStatus();
|
||||
await refreshLicense({ id: activeLicense.id });
|
||||
|
||||
await Promise.all([activeLicenseRefetch()]);
|
||||
activeLicenseRefetch();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
} from 'mocks-server/__mockdata__/roles';
|
||||
import { rest, server } from 'mocks-server/server';
|
||||
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
|
||||
import { fireEvent, render, screen, waitFor } from 'tests/test-utils';
|
||||
import { render, screen, waitFor } from 'tests/test-utils';
|
||||
import {
|
||||
setupAuthzAdmin,
|
||||
setupAuthzDeny,
|
||||
@@ -110,10 +110,7 @@ describe('ServiceAccountDrawer — permissions', () => {
|
||||
it('shows PermissionDeniedCallout in Keys tab when list-keys permission is denied', async () => {
|
||||
server.use(setupAuthzDeny(APIKeyListPermission));
|
||||
|
||||
renderDrawer();
|
||||
await screen.findByDisplayValue('CI Bot');
|
||||
|
||||
fireEvent.click(screen.getByRole('radio', { name: /keys/i }));
|
||||
renderDrawer({ account: 'sa-1', tab: 'keys' });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/list:factor-api-key/)).toBeInTheDocument();
|
||||
|
||||
@@ -28,7 +28,6 @@ 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',
|
||||
|
||||
@@ -453,7 +453,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
if (
|
||||
!isFetchingActiveLicense &&
|
||||
!isNull(activeLicense) &&
|
||||
activeLicense?.event_queue?.event === LicenseEvent.DEFAULT
|
||||
activeLicense?.eventQueue?.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?.event_queue?.scheduled_at).unix() || Date.now(),
|
||||
dayjs(activeLicense?.eventQueue?.scheduledAt).unix() || Date.now(),
|
||||
)}
|
||||
.
|
||||
</span>
|
||||
|
||||
@@ -15,6 +15,11 @@ 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(() => ({
|
||||
|
||||
@@ -30,6 +30,7 @@ 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';
|
||||
@@ -145,6 +146,7 @@ export default function BillingContainer(): JSX.Element {
|
||||
activeLicense,
|
||||
activeLicenseFetchError,
|
||||
} = useAppContext();
|
||||
const { licenseKey } = useActiveLicenseKey();
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
const handleError = useAxiosError();
|
||||
@@ -207,9 +209,9 @@ export default function BillingContainer(): JSX.Element {
|
||||
isFetching: isFetchingBillingData,
|
||||
data: billingData,
|
||||
} = useQuery([REACT_QUERY_KEY.GET_BILLING_USAGE, user?.id], {
|
||||
queryFn: () => getUsage(activeLicense?.key || ''),
|
||||
queryFn: () => getUsage(licenseKey || ''),
|
||||
onError: handleError,
|
||||
enabled: activeLicense !== null,
|
||||
enabled: !!licenseKey,
|
||||
onSuccess: processUsageData,
|
||||
});
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ import {
|
||||
} from 'container/OptionsMenu/constants';
|
||||
import { OptionsQuery } from 'container/OptionsMenu/types';
|
||||
import { ExportDashboard } from 'hooks/dashboard/useExportDashboards';
|
||||
import { useGetSearchQueryParam } from 'hooks/queryBuilder/useGetSearchQueryParam';
|
||||
import { useGetSavedViewParams } from 'hooks/saveViews/useGetSavedViewParams';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useGetAllViews } from 'hooks/saveViews/useGetAllViews';
|
||||
import { useSaveView } from 'hooks/saveViews/useSaveView';
|
||||
@@ -287,8 +287,7 @@ function ExplorerOptions({
|
||||
|
||||
const compositeQuery = mapCompositeQueryFromQuery(currentQuery, panelType);
|
||||
|
||||
const viewName = useGetSearchQueryParam(QueryParams.viewName) || '';
|
||||
const viewKey = useGetSearchQueryParam(QueryParams.viewKey) || '';
|
||||
const { viewName, viewKey } = useGetSavedViewParams();
|
||||
|
||||
const extraData = viewsData?.data?.data?.find(
|
||||
(view) => view.id === viewKey,
|
||||
|
||||
@@ -53,17 +53,24 @@ export const getUpdatedStepInterval = (evalWindow?: string): number => {
|
||||
};
|
||||
|
||||
export const getSelectedQueryOptions = (
|
||||
queries: Array<
|
||||
| IBuilderQuery
|
||||
| IBuilderTraceOperator
|
||||
| IBuilderFormula
|
||||
| IClickHouseQuery
|
||||
| IPromQLQuery
|
||||
>,
|
||||
): SelectProps['options'] =>
|
||||
queries
|
||||
queries:
|
||||
| Array<
|
||||
| IBuilderQuery
|
||||
| IBuilderTraceOperator
|
||||
| IBuilderFormula
|
||||
| IClickHouseQuery
|
||||
| IPromQLQuery
|
||||
>
|
||||
| undefined
|
||||
| null,
|
||||
): SelectProps['options'] => {
|
||||
if (!queries) {
|
||||
return [];
|
||||
}
|
||||
return queries
|
||||
.filter((query) => !query.disabled)
|
||||
.map((query) => ({
|
||||
label: 'queryName' in query ? query.queryName : query.name,
|
||||
value: 'queryName' in query ? query.queryName : query.name,
|
||||
}));
|
||||
};
|
||||
|
||||
@@ -16,6 +16,7 @@ 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,
|
||||
@@ -78,7 +79,8 @@ function GeneralSettings({
|
||||
const [logsCurrentTTLValues, setLogsCurrentTTLValues] =
|
||||
useState(logsTtlValuesPayload);
|
||||
|
||||
const { user, activeLicense } = useAppContext();
|
||||
const { user } = useAppContext();
|
||||
const { licenseKey } = useActiveLicenseKey();
|
||||
|
||||
const [setRetentionPermission] = useComponentPermission(
|
||||
['set_retention_period'],
|
||||
@@ -673,13 +675,13 @@ function GeneralSettings({
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{(showCustomDomainSettings || activeLicense?.key) && (
|
||||
{(showCustomDomainSettings || licenseKey) && (
|
||||
<div className="custom-domain-card">
|
||||
{showCustomDomainSettings && <CustomDomainSettings />}
|
||||
{showCustomDomainSettings && activeLicense?.key && (
|
||||
{showCustomDomainSettings && licenseKey && (
|
||||
<div className="custom-domain-card-divider" />
|
||||
)}
|
||||
{activeLicense?.key && (
|
||||
{licenseKey && (
|
||||
<>
|
||||
<LicenseKeyRow />
|
||||
<LicenseRowDismissibleCallout />
|
||||
|
||||
@@ -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 { useAppContext } from 'providers/App/App';
|
||||
import useActiveLicenseKey from 'hooks/useActiveLicenseKey/useActiveLicenseKey';
|
||||
import { getMaskedKey } from 'utils/maskedKey';
|
||||
|
||||
import './LicenseKeyRow.styles.scss';
|
||||
|
||||
function LicenseKeyRow(): JSX.Element | null {
|
||||
const { activeLicense } = useAppContext();
|
||||
const { licenseKey } = useActiveLicenseKey();
|
||||
const [, copyToClipboard] = useCopyToClipboard();
|
||||
|
||||
if (!activeLicense?.key) {
|
||||
if (!licenseKey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -27,16 +27,14 @@ 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(activeLicense.key)}
|
||||
</code>
|
||||
<code className="license-key-row__code">{getMaskedKey(licenseKey)}</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(activeLicense.key)}
|
||||
onClick={(): void => handleCopyLicenseKey(licenseKey)}
|
||||
>
|
||||
<Copy size={12} />
|
||||
</Button>
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
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', () => ({
|
||||
@@ -23,20 +29,22 @@ describe('LicenseKeyRow', () => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders nothing when activeLicense key is absent', () => {
|
||||
const { container } = render(<LicenseKeyRow />, undefined, {
|
||||
appContextOverrides: { activeLicense: null },
|
||||
it('renders nothing when the license key is absent', () => {
|
||||
mockUseActiveLicenseKey.mockReturnValue({
|
||||
licenseKey: undefined,
|
||||
isLoading: false,
|
||||
});
|
||||
const { container } = render(<LicenseKeyRow />);
|
||||
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it('renders label and masked key when activeLicense key exists', () => {
|
||||
render(<LicenseKeyRow />, undefined, {
|
||||
appContextOverrides: {
|
||||
activeLicense: { key: 'abcdefghij' } as any,
|
||||
},
|
||||
it('renders label and masked key when the license key exists', () => {
|
||||
mockUseActiveLicenseKey.mockReturnValue({
|
||||
licenseKey: 'abcdefghij',
|
||||
isLoading: false,
|
||||
});
|
||||
render(<LicenseKeyRow />);
|
||||
|
||||
expect(screen.getByText('SigNoz License Key')).toBeInTheDocument();
|
||||
expect(screen.getByText('ab·······ij')).toBeInTheDocument();
|
||||
@@ -45,6 +53,10 @@ 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 }));
|
||||
|
||||
@@ -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 apply from 'api/v3/licenses/post';
|
||||
import { activateLicense } from 'api/generated/services/licenses';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import APIError from 'types/api/error';
|
||||
import { toAPIError } from 'utils/errorUtils';
|
||||
import { requireErrorMessage } from 'utils/form/requireErrorMessage';
|
||||
|
||||
import {
|
||||
@@ -26,7 +26,7 @@ function ApplyLicenseForm({
|
||||
|
||||
const isDisabled = isLoading || !key;
|
||||
|
||||
const onFinish = async (values: unknown | { key: string }): Promise<void> => {
|
||||
const onFinish = async (values: unknown): Promise<void> => {
|
||||
const params = values as { key: string };
|
||||
if (params.key === '' || !params.key) {
|
||||
notifications.error({
|
||||
@@ -38,18 +38,19 @@ function ApplyLicenseForm({
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await apply({
|
||||
await activateLicense({
|
||||
key: params.key,
|
||||
});
|
||||
await Promise.all([licenseRefetch()]);
|
||||
licenseRefetch();
|
||||
notifications.success({
|
||||
message: 'Success',
|
||||
description: t('license_applied'),
|
||||
});
|
||||
} catch (e) {
|
||||
const apiError = toAPIError(e as Parameters<typeof toAPIError>[0]);
|
||||
notifications.error({
|
||||
message: (e as APIError).getErrorCode(),
|
||||
description: (e as APIError).getErrorMessage(),
|
||||
message: apiError.getErrorCode(),
|
||||
description: apiError.getErrorMessage(),
|
||||
});
|
||||
}
|
||||
setIsLoading(false);
|
||||
|
||||
@@ -15,9 +15,8 @@ import {
|
||||
QUERY_BUILDER_FUNCTIONS,
|
||||
} from 'constants/antlrQueryConstants';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { useActiveLog } from 'hooks/logs/useActiveLog';
|
||||
import { useGetSearchQueryParam } from 'hooks/queryBuilder/useGetSearchQueryParam';
|
||||
import { useGetSavedViewParams } from 'hooks/saveViews/useGetSavedViewParams';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { ICurrentQueryData } from 'hooks/useHandleExplorerTabChange';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
@@ -50,7 +49,7 @@ function BodyTitleRenderer({
|
||||
const { featureFlags } = useAppContext();
|
||||
const [, setCopy] = useCopyToClipboard();
|
||||
const { notifications } = useNotifications();
|
||||
const viewName = useGetSearchQueryParam(QueryParams.viewName) || '';
|
||||
const { viewName } = useGetSavedViewParams();
|
||||
|
||||
const cleanedNodeKey = removeObjectFromString(nodeKey);
|
||||
const isBodyJsonQueryEnabled =
|
||||
|
||||
@@ -7,13 +7,12 @@ import GroupByIcon from 'assets/CustomIcons/GroupByIcon';
|
||||
import cx from 'classnames';
|
||||
import CopyClipboardHOC from 'components/Logs/CopyClipboardHOC';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { OPERATORS } from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
|
||||
import { RESTRICTED_SELECTED_FIELDS } from 'container/LogsFilters/config';
|
||||
import { MetricsType } from 'container/MetricsApplication/constant';
|
||||
import { useGetSearchQueryParam } from 'hooks/queryBuilder/useGetSearchQueryParam';
|
||||
import { useGetSavedViewParams } from 'hooks/saveViews/useGetSavedViewParams';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { ICurrentQueryData } from 'hooks/useHandleExplorerTabChange';
|
||||
import {
|
||||
@@ -141,7 +140,7 @@ export default function TableViewActions(
|
||||
|
||||
const { pathname } = useLocation();
|
||||
const { stagedQuery, updateQueriesData } = useQueryBuilder();
|
||||
const viewName = useGetSearchQueryParam(QueryParams.viewName) || '';
|
||||
const { viewName } = useGetSavedViewParams();
|
||||
const { dataType, logType: fieldType } = getFieldAttributes(record.field);
|
||||
|
||||
// there is no option for where clause in old logs explorer and live logs page or infra monitoring
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { RESTRICTED_SELECTED_FIELDS } from 'container/LogsFilters/config';
|
||||
import { useGetSearchQueryParam } from 'hooks/queryBuilder/useGetSearchQueryParam';
|
||||
import { useGetSavedViewParams } from 'hooks/saveViews/useGetSavedViewParams';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { ExplorerViews } from 'pages/LogsExplorer/utils';
|
||||
|
||||
@@ -88,7 +88,7 @@ jest.mock('react-router-dom', () => ({
|
||||
}));
|
||||
|
||||
jest.mock('hooks/queryBuilder/useQueryBuilder');
|
||||
jest.mock('hooks/queryBuilder/useGetSearchQueryParam');
|
||||
jest.mock('hooks/saveViews/useGetSavedViewParams');
|
||||
|
||||
describe('TableViewActions', () => {
|
||||
const TEST_VALUE = 'test value';
|
||||
@@ -140,8 +140,10 @@ describe('TableViewActions', () => {
|
||||
}),
|
||||
} as any);
|
||||
|
||||
// Default mock for useGetSearchQueryParam
|
||||
jest.mocked(useGetSearchQueryParam).mockReturnValue(null);
|
||||
// Default mock for useGetSavedViewParams
|
||||
jest
|
||||
.mocked(useGetSavedViewParams)
|
||||
.mockReturnValue({ viewName: '', viewKey: '' });
|
||||
});
|
||||
|
||||
it('should render without crashing', () => {
|
||||
@@ -249,7 +251,9 @@ describe('TableViewActions', () => {
|
||||
updateQueriesData: mockUpdateQueriesData,
|
||||
} as any);
|
||||
|
||||
jest.mocked(useGetSearchQueryParam).mockReturnValue(null);
|
||||
jest
|
||||
.mocked(useGetSavedViewParams)
|
||||
.mockReturnValue({ viewName: '', viewKey: '' });
|
||||
|
||||
render(
|
||||
<TableViewActions
|
||||
|
||||
@@ -3,10 +3,9 @@ import { useLocation } from 'react-router-dom';
|
||||
import { CircleMinus, CirclePlus, Layers, RefreshCw } from '@signozhq/icons';
|
||||
import { convertFiltersToExpression } from 'components/QueryBuilderV2/utils';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
|
||||
import { useGetSearchQueryParam } from 'hooks/queryBuilder/useGetSearchQueryParam';
|
||||
import { useGetSavedViewParams } from 'hooks/saveViews/useGetSavedViewParams';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { ICurrentQueryData } from 'hooks/useHandleExplorerTabChange';
|
||||
import { ExplorerViews } from 'pages/LogsExplorer/utils';
|
||||
@@ -58,7 +57,7 @@ export function useLogAttributeActions({
|
||||
const { pathname } = useLocation();
|
||||
const { stagedQuery, updateQueriesData } = useQueryBuilder();
|
||||
const { featureFlags } = useAppContext();
|
||||
const viewName = useGetSearchQueryParam(QueryParams.viewName) || '';
|
||||
const { viewName } = useGetSavedViewParams();
|
||||
|
||||
const isBodyJsonQueryEnabled =
|
||||
featureFlags?.find((flag) => flag.name === FeatureKeys.USE_JSON_BODY)
|
||||
|
||||
@@ -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 { useAppContext } from 'providers/App/App';
|
||||
import useActiveLicenseKey from 'hooks/useActiveLicenseKey/useActiveLicenseKey';
|
||||
import { getMaskedKey } from 'utils/maskedKey';
|
||||
|
||||
import './LicenseSection.styles.scss';
|
||||
|
||||
function LicenseSection(): JSX.Element | null {
|
||||
const { activeLicense } = useAppContext();
|
||||
const { licenseKey } = useActiveLicenseKey();
|
||||
const { notifications } = useNotifications();
|
||||
const [, handleCopyToClipboard] = useCopyToClipboard();
|
||||
|
||||
@@ -20,7 +20,7 @@ function LicenseSection(): JSX.Element | null {
|
||||
});
|
||||
};
|
||||
|
||||
if (!activeLicense?.key) {
|
||||
if (!licenseKey) {
|
||||
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(activeLicense.key)}</Typography.Text>
|
||||
<Typography.Text code>{getMaskedKey(licenseKey)}</Typography.Text>
|
||||
<Button
|
||||
variant="link"
|
||||
color="none"
|
||||
aria-label="Copy license key"
|
||||
data-testid="license-key-copy-btn"
|
||||
onClick={(): void => handleCopyKey(activeLicense.key)}
|
||||
onClick={(): void => handleCopyKey(licenseKey)}
|
||||
>
|
||||
<Copy size={14} />
|
||||
</Button>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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,
|
||||
@@ -12,6 +13,11 @@ 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();
|
||||
@@ -87,6 +93,10 @@ describe('MySettings Flows', () => {
|
||||
jest.clearAllMocks();
|
||||
editUserFn.mockResolvedValue({});
|
||||
updateMyPasswordFn.mockResolvedValue({});
|
||||
mockUseActiveLicenseKey.mockReturnValue({
|
||||
licenseKey: 'test-key',
|
||||
isLoading: false,
|
||||
});
|
||||
render(<MySettingsContainer />);
|
||||
});
|
||||
|
||||
@@ -368,11 +378,11 @@ describe('MySettings Flows', () => {
|
||||
});
|
||||
|
||||
it('Should not render license section when license key is missing', () => {
|
||||
const { container } = render(<MySettingsContainer />, undefined, {
|
||||
appContextOverrides: {
|
||||
activeLicense: null,
|
||||
},
|
||||
mockUseActiveLicenseKey.mockReturnValue({
|
||||
licenseKey: undefined,
|
||||
isLoading: false,
|
||||
});
|
||||
const { container } = render(<MySettingsContainer />);
|
||||
|
||||
const scoped = within(container);
|
||||
expect(scoped.queryByText('License')).not.toBeInTheDocument();
|
||||
@@ -383,38 +393,32 @@ describe('MySettings Flows', () => {
|
||||
});
|
||||
|
||||
it('Should mask license key in the UI', () => {
|
||||
const { container } = render(<MySettingsContainer />, undefined, {
|
||||
appContextOverrides: {
|
||||
activeLicense: {
|
||||
key: 'abcd',
|
||||
} as any,
|
||||
},
|
||||
mockUseActiveLicenseKey.mockReturnValue({
|
||||
licenseKey: 'abcd',
|
||||
isLoading: false,
|
||||
});
|
||||
const { container } = render(<MySettingsContainer />);
|
||||
|
||||
expect(within(container).getByText('ab·······cd')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should not mask license key if it is too short', () => {
|
||||
const { container } = render(<MySettingsContainer />, undefined, {
|
||||
appContextOverrides: {
|
||||
activeLicense: {
|
||||
key: 'abc',
|
||||
} as any,
|
||||
},
|
||||
mockUseActiveLicenseKey.mockReturnValue({
|
||||
licenseKey: 'abc',
|
||||
isLoading: false,
|
||||
});
|
||||
const { container } = render(<MySettingsContainer />);
|
||||
|
||||
expect(within(container).getByText('abc')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should copy license key and show success toast', async () => {
|
||||
const user = userEvent.setup();
|
||||
const { container } = render(<MySettingsContainer />, undefined, {
|
||||
appContextOverrides: {
|
||||
activeLicense: {
|
||||
key: 'test-license-key-12345',
|
||||
} as any,
|
||||
},
|
||||
mockUseActiveLicenseKey.mockReturnValue({
|
||||
licenseKey: 'test-license-key-12345',
|
||||
isLoading: false,
|
||||
});
|
||||
const { container } = render(<MySettingsContainer />);
|
||||
|
||||
await user.click(within(container).getByTestId('license-key-copy-btn'));
|
||||
|
||||
|
||||
@@ -329,11 +329,12 @@ describe('transformTransactionGroupsToResourcePermissions', () => {
|
||||
it('returns all resources from RESOURCE_ORDER even with empty transaction groups', () => {
|
||||
const result = transformTransactionGroupsToResourcePermissions([]);
|
||||
|
||||
expect(result).toHaveLength(7);
|
||||
expect(result).toHaveLength(8);
|
||||
expect(result.map((r) => r.resourceKind)).toStrictEqual([
|
||||
'factor-api-key',
|
||||
'role',
|
||||
'serviceaccount',
|
||||
'license',
|
||||
'logs',
|
||||
'traces',
|
||||
'metrics',
|
||||
@@ -418,11 +419,12 @@ describe('createEmptyRolePermissions', () => {
|
||||
it('creates permissions for all resources in RESOURCE_ORDER', () => {
|
||||
const result = createEmptyRolePermissions();
|
||||
|
||||
expect(result).toHaveLength(7);
|
||||
expect(result).toHaveLength(8);
|
||||
expect(result.map((r) => r.resourceKind)).toStrictEqual([
|
||||
'factor-api-key',
|
||||
'role',
|
||||
'serviceaccount',
|
||||
'license',
|
||||
'logs',
|
||||
'traces',
|
||||
'metrics',
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
Bot,
|
||||
ChartLine,
|
||||
DraftingCompass,
|
||||
FileKey,
|
||||
Gauge,
|
||||
Key,
|
||||
Logs,
|
||||
@@ -61,6 +62,13 @@ 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.',
|
||||
|
||||
@@ -27,6 +27,14 @@ export const useGetCompositeQueryParam = (): Query | null => {
|
||||
decodeURIComponent(compositeQuery.replace(/\+/g, ' ')),
|
||||
);
|
||||
|
||||
// Add default values for optional fields if empty
|
||||
if (parsedCompositeQuery?.builder) {
|
||||
parsedCompositeQuery.builder.queryFormulas =
|
||||
parsedCompositeQuery.builder.queryFormulas ?? [];
|
||||
parsedCompositeQuery.builder.queryTraceOperator =
|
||||
parsedCompositeQuery.builder.queryTraceOperator ?? [];
|
||||
}
|
||||
|
||||
// Convert old format to new format for each query in builder.queryData
|
||||
if (parsedCompositeQuery?.builder?.queryData) {
|
||||
parsedCompositeQuery.builder.queryData =
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
import { QuerySearchParamNames } from 'components/ExplorerCard/constants';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
|
||||
export const useGetSearchQueryParam = (
|
||||
searchParams: QuerySearchParamNames,
|
||||
): string | null => {
|
||||
const urlQuery = useUrlQuery();
|
||||
|
||||
return useMemo(() => {
|
||||
const searchQuery = urlQuery.get(searchParams);
|
||||
|
||||
return searchQuery ? JSON.parse(searchQuery) : null;
|
||||
}, [urlQuery, searchParams]);
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
|
||||
import { useGetSavedViewParams } from '../useGetSavedViewParams';
|
||||
|
||||
jest.mock('hooks/useUrlQuery');
|
||||
|
||||
const mockedUseUrlQuery = useUrlQuery as jest.Mock;
|
||||
|
||||
const setSearch = (search: string): void => {
|
||||
mockedUseUrlQuery.mockReturnValue(new URLSearchParams(search));
|
||||
};
|
||||
|
||||
describe('useGetSavedViewParams', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('returns empty strings when no params are present', () => {
|
||||
setSearch('');
|
||||
|
||||
const { result } = renderHook(() => useGetSavedViewParams());
|
||||
|
||||
expect(result.current).toStrictEqual({ viewName: '', viewKey: '' });
|
||||
});
|
||||
|
||||
it('parses JSON-stringified values', () => {
|
||||
setSearch(
|
||||
`viewName=${encodeURIComponent(
|
||||
JSON.stringify('Hindsight'),
|
||||
)}&viewKey=${encodeURIComponent(JSON.stringify('abc-123'))}`,
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useGetSavedViewParams());
|
||||
|
||||
expect(result.current).toStrictEqual({
|
||||
viewName: 'Hindsight',
|
||||
viewKey: 'abc-123',
|
||||
});
|
||||
});
|
||||
|
||||
it('falls back to the raw string when a value is not valid JSON', () => {
|
||||
setSearch('viewName=Hindsight&viewKey=some-uuid-value');
|
||||
|
||||
const { result } = renderHook(() => useGetSavedViewParams());
|
||||
|
||||
expect(result.current).toStrictEqual({
|
||||
viewName: 'Hindsight',
|
||||
viewKey: 'some-uuid-value',
|
||||
});
|
||||
});
|
||||
|
||||
it('does not throw and keeps the raw string for non-string JSON', () => {
|
||||
setSearch('viewName=123');
|
||||
|
||||
const { result } = renderHook(() => useGetSavedViewParams());
|
||||
|
||||
expect(result.current).toStrictEqual({ viewName: '123', viewKey: '' });
|
||||
});
|
||||
});
|
||||
33
frontend/src/hooks/saveViews/useGetSavedViewParams.ts
Normal file
33
frontend/src/hooks/saveViews/useGetSavedViewParams.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { useMemo } from 'react';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
|
||||
interface SavedViewParams {
|
||||
viewName: string;
|
||||
viewKey: string;
|
||||
}
|
||||
|
||||
const parseViewParam = (value: string | null): string => {
|
||||
if (!value) {
|
||||
return '';
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
return typeof parsed === 'string' ? parsed : value;
|
||||
} catch {
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
export const useGetSavedViewParams = (): SavedViewParams => {
|
||||
const urlQuery = useUrlQuery();
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
viewName: parseViewParam(urlQuery.get(QueryParams.viewName)),
|
||||
viewKey: parseViewParam(urlQuery.get(QueryParams.viewKey)),
|
||||
}),
|
||||
[urlQuery],
|
||||
);
|
||||
};
|
||||
29
frontend/src/hooks/useActiveLicense/useActiveLicense.tsx
Normal file
29
frontend/src/hooks/useActiveLicense/useActiveLicense.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
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;
|
||||
37
frontend/src/hooks/useActiveLicense/utils.ts
Normal file
37
frontend/src/hooks/useActiveLicense/utils.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
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,
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
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;
|
||||
@@ -1,18 +0,0 @@
|
||||
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;
|
||||
@@ -6,7 +6,7 @@ import { SIGNOZ_VALUE } from 'container/QueryBuilder/filters/OrderByFilter/const
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { useGetSearchQueryParam } from './queryBuilder/useGetSearchQueryParam';
|
||||
import { useGetSavedViewParams } from './saveViews/useGetSavedViewParams';
|
||||
import { useQueryBuilder } from './queryBuilder/useQueryBuilder';
|
||||
|
||||
export interface ICurrentQueryData {
|
||||
@@ -31,9 +31,7 @@ export const useHandleExplorerTabChange = (): {
|
||||
updateQueriesData,
|
||||
} = useQueryBuilder();
|
||||
|
||||
const viewName = useGetSearchQueryParam(QueryParams.viewName) || '';
|
||||
|
||||
const viewKey = useGetSearchQueryParam(QueryParams.viewKey) || '';
|
||||
const { viewName, viewKey } = useGetSavedViewParams();
|
||||
|
||||
const getUpdateQuery = useCallback(
|
||||
(newPanelType: PANEL_TYPES): Query => {
|
||||
|
||||
@@ -8,6 +8,19 @@ 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',
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
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}`);
|
||||
@@ -127,30 +127,30 @@ export function buildLicense(
|
||||
overrides?: Partial<LicenseResModel>,
|
||||
): LicenseResModel {
|
||||
return {
|
||||
key: 'test-key',
|
||||
id: 'test-license-id',
|
||||
status: LicenseStatus.VALID,
|
||||
state: LicenseState.ACTIVATED,
|
||||
platform: LicensePlatform.CLOUD,
|
||||
event_queue: {
|
||||
created_at: '0',
|
||||
eventQueue: {
|
||||
createdAt: '0',
|
||||
event: LicenseEvent.NO_EVENT,
|
||||
scheduled_at: '0',
|
||||
scheduledAt: '0',
|
||||
status: '',
|
||||
updated_at: '0',
|
||||
updatedAt: '0',
|
||||
},
|
||||
plan: {
|
||||
created_at: '0',
|
||||
id: '0',
|
||||
createdAt: '0',
|
||||
description: '',
|
||||
is_active: true,
|
||||
isActive: true,
|
||||
name: '',
|
||||
updated_at: '0',
|
||||
updatedAt: '0',
|
||||
},
|
||||
plan_id: '0',
|
||||
free_until: '0',
|
||||
updated_at: '0',
|
||||
valid_from: 0,
|
||||
valid_until: 0,
|
||||
created_at: '0',
|
||||
freeUntil: '0',
|
||||
updatedAt: '0',
|
||||
validFrom: 0,
|
||||
validUntil: 0,
|
||||
createdAt: '0',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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 useActiveLicenseV3 from 'hooks/useActiveLicenseV3/useActiveLicenseV3';
|
||||
import useActiveLicense from 'hooks/useActiveLicense/useActiveLicense';
|
||||
import {
|
||||
IsAdminPermission,
|
||||
IsEditorPermission,
|
||||
@@ -210,35 +210,34 @@ export function AppProvider({ children }: PropsWithChildren): JSX.Element {
|
||||
}
|
||||
}, [userData, isFetchingUserData]);
|
||||
|
||||
// fetcher for licenses v3
|
||||
// fetcher for the active license
|
||||
const {
|
||||
data: activeLicenseData,
|
||||
isFetching: isFetchingActiveLicense,
|
||||
error: activeLicenseFetchError,
|
||||
refetch: activeLicenseRefetch,
|
||||
} = useActiveLicenseV3(isLoggedIn);
|
||||
} = useActiveLicense(isLoggedIn);
|
||||
useEffect(() => {
|
||||
if (!isFetchingActiveLicense && activeLicenseData && activeLicenseData.data) {
|
||||
setActiveLicense(activeLicenseData.data);
|
||||
if (!isFetchingActiveLicense && activeLicenseData) {
|
||||
setActiveLicense(activeLicenseData);
|
||||
|
||||
const isOnTrial = dayjs(
|
||||
activeLicenseData.data.free_until || Date.now(),
|
||||
).isAfter(dayjs());
|
||||
const freeUntilUnix = dayjs(activeLicenseData.freeUntil).unix();
|
||||
const scheduledAtUnix = dayjs(
|
||||
activeLicenseData.eventQueue.scheduledAt,
|
||||
).unix();
|
||||
|
||||
const trialInfo: TrialInfo = {
|
||||
trialStart: activeLicenseData.data.valid_from,
|
||||
trialEnd: dayjs(activeLicenseData.data.free_until || Date.now()).unix(),
|
||||
onTrial: isOnTrial,
|
||||
trialStart: activeLicenseData.validFrom,
|
||||
trialEnd: freeUntilUnix > 0 ? freeUntilUnix : dayjs().unix(),
|
||||
onTrial: dayjs(activeLicenseData.freeUntil).isAfter(dayjs()),
|
||||
workSpaceBlock:
|
||||
activeLicenseData.data.state === LicenseState.EVALUATION_EXPIRED &&
|
||||
activeLicenseData.data.platform === LicensePlatform.CLOUD,
|
||||
activeLicenseData.state === LicenseState.EVALUATION_EXPIRED &&
|
||||
activeLicenseData.platform === LicensePlatform.CLOUD,
|
||||
trialConvertedToSubscription:
|
||||
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(),
|
||||
activeLicenseData.state !== LicenseState.ISSUED &&
|
||||
activeLicenseData.state !== LicenseState.EVALUATING &&
|
||||
activeLicenseData.state !== LicenseState.EVALUATION_EXPIRED,
|
||||
gracePeriodEnd: scheduledAtUnix > 0 ? scheduledAtUnix : dayjs().unix(),
|
||||
};
|
||||
|
||||
setTrialInfo(trialInfo);
|
||||
|
||||
@@ -163,20 +163,23 @@ export function QueryBuilderProvider({
|
||||
const prepareQueryBuilderData = useCallback(
|
||||
(query: Query): Query => {
|
||||
const builder: QueryBuilderData = {
|
||||
queryData: query.builder.queryData?.map((item) => ({
|
||||
...initialQueryBuilderFormValuesMap[
|
||||
initialDataSource || DataSource.METRICS
|
||||
],
|
||||
...item,
|
||||
})),
|
||||
queryFormulas: query.builder.queryFormulas?.map((item) => ({
|
||||
...initialFormulaBuilderFormValues,
|
||||
...item,
|
||||
})),
|
||||
queryTraceOperator: query.builder.queryTraceOperator?.map((item) => ({
|
||||
...initialQueryBuilderFormTraceOperatorValues,
|
||||
...item,
|
||||
})),
|
||||
queryData:
|
||||
query.builder.queryData?.map((item) => ({
|
||||
...initialQueryBuilderFormValuesMap[
|
||||
initialDataSource || DataSource.METRICS
|
||||
],
|
||||
...item,
|
||||
})) ?? [],
|
||||
queryFormulas:
|
||||
query.builder.queryFormulas?.map((item) => ({
|
||||
...initialFormulaBuilderFormValues,
|
||||
...item,
|
||||
})) ?? [],
|
||||
queryTraceOperator:
|
||||
query.builder.queryTraceOperator?.map((item) => ({
|
||||
...initialQueryBuilderFormTraceOperatorValues,
|
||||
...item,
|
||||
})) ?? [],
|
||||
};
|
||||
|
||||
const setupedQueryData = builder.queryData.map((item) => {
|
||||
@@ -209,15 +212,17 @@ export function QueryBuilderProvider({
|
||||
return currentElement;
|
||||
});
|
||||
|
||||
const promql: IPromQLQuery[] = query.promql.map((item) => ({
|
||||
...initialQueryPromQLData,
|
||||
...item,
|
||||
}));
|
||||
const promql: IPromQLQuery[] =
|
||||
query.promql?.map((item) => ({
|
||||
...initialQueryPromQLData,
|
||||
...item,
|
||||
})) ?? [];
|
||||
|
||||
const clickHouse: IClickHouseQuery[] = query.clickhouse_sql.map((item) => ({
|
||||
...initialClickHouseData,
|
||||
...item,
|
||||
}));
|
||||
const clickHouse: IClickHouseQuery[] =
|
||||
query.clickhouse_sql?.map((item) => ({
|
||||
...initialClickHouseData,
|
||||
...item,
|
||||
})) ?? [];
|
||||
|
||||
const newQueryState: QueryState = {
|
||||
clickhouse_sql: clickHouse,
|
||||
|
||||
@@ -66,6 +66,7 @@
|
||||
"factor-api-key",
|
||||
"license",
|
||||
"subscription",
|
||||
"deployment-host",
|
||||
"logs",
|
||||
"traces",
|
||||
"metrics",
|
||||
|
||||
@@ -158,30 +158,30 @@ export function getAppContextMock(
|
||||
): IAppContext {
|
||||
return {
|
||||
activeLicense: {
|
||||
key: 'test-key',
|
||||
event_queue: {
|
||||
created_at: '0',
|
||||
id: 'test-license-id',
|
||||
eventQueue: {
|
||||
createdAt: '0',
|
||||
event: LicenseEvent.NO_EVENT,
|
||||
scheduled_at: '0',
|
||||
scheduledAt: '0',
|
||||
status: '',
|
||||
updated_at: '0',
|
||||
updatedAt: '0',
|
||||
},
|
||||
state: LicenseState.ACTIVATED,
|
||||
status: LicenseStatus.VALID,
|
||||
platform: LicensePlatform.CLOUD,
|
||||
created_at: '0',
|
||||
createdAt: '0',
|
||||
plan: {
|
||||
created_at: '0',
|
||||
id: '0',
|
||||
createdAt: '0',
|
||||
description: '',
|
||||
is_active: true,
|
||||
isActive: true,
|
||||
name: '',
|
||||
updated_at: '0',
|
||||
updatedAt: '0',
|
||||
},
|
||||
plan_id: '0',
|
||||
free_until: '0',
|
||||
updated_at: '0',
|
||||
valid_from: 0,
|
||||
valid_until: 0,
|
||||
freeUntil: '0',
|
||||
updatedAt: '0',
|
||||
validFrom: 0,
|
||||
validUntil: 0,
|
||||
},
|
||||
trialInfo: {
|
||||
trialStart: -1,
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import { License } from './def';
|
||||
|
||||
export interface Props {
|
||||
key: string;
|
||||
}
|
||||
|
||||
export interface PayloadProps {
|
||||
status: string;
|
||||
data: License;
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
export interface License {
|
||||
key: string;
|
||||
ValidFrom: Date;
|
||||
ValidUntil: Date;
|
||||
planKey: string;
|
||||
status: string;
|
||||
isCurrent: boolean;
|
||||
}
|
||||
@@ -1,57 +1,59 @@
|
||||
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;
|
||||
scheduled_at: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
scheduledAt: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type LicenseResModel = {
|
||||
key: string;
|
||||
id: string;
|
||||
status: LicenseStatus;
|
||||
state: LicenseState;
|
||||
event_queue: LicenseEventQueueResModel;
|
||||
platform: LicensePlatform;
|
||||
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;
|
||||
plan: LicensePlanResModel;
|
||||
eventQueue: LicenseEventQueueResModel;
|
||||
freeUntil: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
validFrom: number;
|
||||
validUntil: number;
|
||||
};
|
||||
|
||||
// Duplicate of old licenses API response, need to improve this later
|
||||
@@ -63,8 +65,3 @@ export type TrialInfo = {
|
||||
trialConvertedToSubscription: boolean;
|
||||
gracePeriodEnd: number;
|
||||
};
|
||||
|
||||
export interface PayloadProps {
|
||||
data: LicenseEventQueueResModel;
|
||||
status: string;
|
||||
}
|
||||
|
||||
165
pkg/apiserver/signozapiserver/licensing.go
Normal file
165
pkg/apiserver/signozapiserver/licensing.go
Normal file
@@ -0,0 +1,165 @@
|
||||
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
|
||||
}
|
||||
@@ -12,6 +12,7 @@ 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"
|
||||
@@ -67,6 +68,7 @@ type provider struct {
|
||||
authzHandler authz.Handler
|
||||
rawDataExportHandler rawdataexport.Handler
|
||||
zeusHandler zeus.Handler
|
||||
licensingHandler licensing.Handler
|
||||
querierHandler querier.Handler
|
||||
serviceAccountHandler serviceaccount.Handler
|
||||
serviceAccountGetter serviceaccount.Getter
|
||||
@@ -105,6 +107,7 @@ func NewFactory(
|
||||
authzHandler authz.Handler,
|
||||
rawDataExportHandler rawdataexport.Handler,
|
||||
zeusHandler zeus.Handler,
|
||||
licensingHandler licensing.Handler,
|
||||
querierHandler querier.Handler,
|
||||
serviceAccountHandler serviceaccount.Handler,
|
||||
serviceAccountGetter serviceaccount.Getter,
|
||||
@@ -146,6 +149,7 @@ func NewFactory(
|
||||
authzHandler,
|
||||
rawDataExportHandler,
|
||||
zeusHandler,
|
||||
licensingHandler,
|
||||
querierHandler,
|
||||
serviceAccountHandler,
|
||||
serviceAccountGetter,
|
||||
@@ -189,6 +193,7 @@ func newProvider(
|
||||
authzHandler authz.Handler,
|
||||
rawDataExportHandler rawdataexport.Handler,
|
||||
zeusHandler zeus.Handler,
|
||||
licensingHandler licensing.Handler,
|
||||
querierHandler querier.Handler,
|
||||
serviceAccountHandler serviceaccount.Handler,
|
||||
serviceAccountGetter serviceaccount.Getter,
|
||||
@@ -231,6 +236,7 @@ func newProvider(
|
||||
authzHandler: authzHandler,
|
||||
rawDataExportHandler: rawDataExportHandler,
|
||||
zeusHandler: zeusHandler,
|
||||
licensingHandler: licensingHandler,
|
||||
querierHandler: querierHandler,
|
||||
serviceAccountHandler: serviceAccountHandler,
|
||||
serviceAccountGetter: serviceAccountGetter,
|
||||
@@ -332,6 +338,10 @@ 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
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/http/handler"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/zeustypes"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
@@ -27,7 +29,7 @@ func (provider *provider) addZeusRoutes(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/zeus/hosts", handler.New(provider.authzMiddleware.ViewAccess(provider.zeusHandler.GetHosts), handler.OpenAPIDef{
|
||||
if err := router.Handle("/api/v2/zeus/hosts", handler.New(provider.authzMiddleware.CheckResources(provider.zeusHandler.GetHosts, authtypes.SigNozAdminRoleName, authtypes.SigNozEditorRoleName, authtypes.SigNozViewerRoleName), handler.OpenAPIDef{
|
||||
ID: "GetHosts",
|
||||
Tags: []string{"zeus"},
|
||||
Summary: "Get host info from Zeus.",
|
||||
@@ -39,12 +41,17 @@ func (provider *provider) addZeusRoutes(router *mux.Router) error {
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusForbidden, http.StatusNotFound},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
})).Methods(http.MethodGet).GetError(); err != nil {
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceDeploymentHost.Scope(coretypes.VerbList)}),
|
||||
}, handler.WithResourceDefs(handler.BasicResourceDef{
|
||||
Resource: coretypes.ResourceMetaResourceDeploymentHost,
|
||||
Verb: coretypes.VerbList,
|
||||
Category: coretypes.ActionCategoryDataAccess,
|
||||
Selector: coretypes.WildcardSelector,
|
||||
}))).Methods(http.MethodGet).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/zeus/hosts", handler.New(provider.authzMiddleware.AdminAccess(provider.zeusHandler.PutHost), handler.OpenAPIDef{
|
||||
if err := router.Handle("/api/v2/zeus/hosts", handler.New(provider.authzMiddleware.CheckResources(provider.zeusHandler.PutHost, authtypes.SigNozAdminRoleName), handler.OpenAPIDef{
|
||||
ID: "PutHost",
|
||||
Tags: []string{"zeus"},
|
||||
Summary: "Put host in Zeus for a deployment.",
|
||||
@@ -56,8 +63,14 @@ func (provider *provider) addZeusRoutes(router *mux.Router) error {
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusForbidden, http.StatusNotFound, http.StatusConflict},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
})).Methods(http.MethodPut).GetError(); err != nil {
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceDeploymentHost.Scope(coretypes.VerbUpdate)}),
|
||||
}, handler.WithResourceDefs(handler.BasicResourceDef{
|
||||
Resource: coretypes.ResourceMetaResourceDeploymentHost,
|
||||
Verb: coretypes.VerbUpdate,
|
||||
Category: coretypes.ActionCategoryConfigurationChange,
|
||||
ID: coretypes.BodyJSONPath("name"),
|
||||
Selector: coretypes.WildcardSelector,
|
||||
}))).Methods(http.MethodPut).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
169
pkg/licensing/handler.go
Normal file
169
pkg/licensing/handler.go
Normal file
@@ -0,0 +1,169 @@
|
||||
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))
|
||||
}
|
||||
@@ -21,10 +21,16 @@ type Licensing interface {
|
||||
|
||||
// Validate validates the license with the upstream server
|
||||
Validate(ctx context.Context) error
|
||||
// Activate validates and enables the license
|
||||
// Activate validates the key with the upstream server 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
|
||||
@@ -38,10 +44,20 @@ 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)
|
||||
}
|
||||
|
||||
@@ -14,18 +14,6 @@ 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"))
|
||||
}
|
||||
|
||||
@@ -39,6 +39,18 @@ 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")
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package prometheus
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/prometheus/promql"
|
||||
)
|
||||
@@ -23,5 +24,11 @@ func NewEngine(logger *slog.Logger, cfg Config) *Engine {
|
||||
Timeout: cfg.Timeout,
|
||||
ActiveQueryTracker: activeQueryTracker,
|
||||
LookbackDelta: cfg.LookbackDelta,
|
||||
// The engine calls this for subqueries that do not set a step, such as
|
||||
// `metric[5m:]`, and segfaults if it is nil. 1m matches the default
|
||||
// global evaluation_interval that Prometheus wires here.
|
||||
NoStepSubqueryIntervalFn: func(int64) int64 {
|
||||
return time.Minute.Milliseconds()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
33
pkg/prometheus/engine_test.go
Normal file
33
pkg/prometheus/engine_test.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package prometheus
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/prometheus/storage"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNoStepSubqueryDoesNotPanic(t *testing.T) {
|
||||
engine := NewEngine(slog.New(slog.DiscardHandler), Config{Timeout: time.Minute})
|
||||
queryable := storage.QueryableFunc(func(int64, int64) (storage.Querier, error) {
|
||||
return storage.NoopQuerier(), nil
|
||||
})
|
||||
|
||||
qry, err := engine.NewRangeQuery(
|
||||
context.Background(),
|
||||
queryable,
|
||||
nil,
|
||||
"max_over_time(some_metric[5m:])",
|
||||
time.Now().Add(-time.Hour),
|
||||
time.Now(),
|
||||
time.Minute,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
defer qry.Close()
|
||||
|
||||
res := qry.Exec(context.Background())
|
||||
require.NoError(t, res.Err)
|
||||
}
|
||||
@@ -457,13 +457,6 @@ func (aH *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
|
||||
|
||||
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
|
||||
|
||||
@@ -77,6 +77,7 @@ type Handlers struct {
|
||||
AIObservability aiobservability.Handler
|
||||
AuthzHandler authz.Handler
|
||||
ZeusHandler zeus.Handler
|
||||
LicensingHandler licensing.Handler
|
||||
QuerierHandler querier.Handler
|
||||
ServiceAccountHandler serviceaccount.Handler
|
||||
RegistryHandler factory.Handler
|
||||
@@ -95,7 +96,7 @@ func NewHandlers(
|
||||
providerSettings factory.ProviderSettings,
|
||||
analytics analytics.Analytics,
|
||||
querierHandler querier.Handler,
|
||||
licensing licensing.Licensing,
|
||||
licensingService licensing.Licensing,
|
||||
global global.Global,
|
||||
flaggerService flagger.Flagger,
|
||||
gatewayService gateway.Gateway,
|
||||
@@ -125,7 +126,8 @@ func NewHandlers(
|
||||
Fields: implfields.NewHandler(providerSettings, telemetryMetadataStore),
|
||||
AIObservability: implaiobservability.NewHandler(telemetryMetadataStore),
|
||||
AuthzHandler: signozauthzapi.NewHandler(authz),
|
||||
ZeusHandler: zeus.NewHandler(zeusService, licensing),
|
||||
ZeusHandler: zeus.NewHandler(zeusService, licensingService),
|
||||
LicensingHandler: licensing.NewHandler(licensingService),
|
||||
QuerierHandler: querierHandler,
|
||||
ServiceAccountHandler: implserviceaccount.NewHandler(modules.ServiceAccount, modules.ServiceAccountGetter),
|
||||
RegistryHandler: registryHandler,
|
||||
|
||||
@@ -17,6 +17,7 @@ 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"
|
||||
@@ -80,6 +81,7 @@ 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 }{},
|
||||
|
||||
@@ -244,6 +244,8 @@ func NewSQLMigrationProviderFactories(
|
||||
sqlmigration.NewDeleteOrphanUserRolesFactory(),
|
||||
sqlmigration.NewMigrateLambdaDashboardsFactory(),
|
||||
sqlmigration.NewAddAuthDomainTuplesFactory(sqlstore),
|
||||
sqlmigration.NewAddDeploymentHostTuplesFactory(sqlstore),
|
||||
sqlmigration.NewAddLicenseTuplesFactory(sqlstore),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -334,6 +336,7 @@ func NewAPIServerProviderFactories(orgGetter organization.Getter, authz authz.Au
|
||||
handlers.AuthzHandler,
|
||||
handlers.RawDataExport,
|
||||
handlers.ZeusHandler,
|
||||
handlers.LicensingHandler,
|
||||
handlers.QuerierHandler,
|
||||
handlers.ServiceAccountHandler,
|
||||
modules.ServiceAccountGetter,
|
||||
|
||||
161
pkg/sqlmigration/118_add_deployment_host_tuples.go
Normal file
161
pkg/sqlmigration/118_add_deployment_host_tuples.go
Normal file
@@ -0,0 +1,161 @@
|
||||
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 addDeploymentHostTuples struct {
|
||||
sqlstore sqlstore.SQLStore
|
||||
}
|
||||
|
||||
func NewAddDeploymentHostTuplesFactory(sqlstore sqlstore.SQLStore) factory.ProviderFactory[SQLMigration, Config] {
|
||||
return factory.NewProviderFactory(factory.MustNewName("add_deployment_host_tuples"), func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
|
||||
return &addDeploymentHostTuples{sqlstore: sqlstore}, nil
|
||||
})
|
||||
}
|
||||
|
||||
func (migration *addDeploymentHostTuples) Register(migrations *migrate.Migrations) error {
|
||||
return migrations.Register(migration.Up, migration.Down)
|
||||
}
|
||||
|
||||
func (migration *addDeploymentHostTuples) Up(ctx context.Context, db *bun.DB) error {
|
||||
tx, err := db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = tx.Rollback() }()
|
||||
|
||||
var storeID string
|
||||
err = tx.QueryRowContext(ctx, `SELECT id FROM store WHERE name = ? LIMIT 1`, "signoz").Scan(&storeID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var orgIDs []string
|
||||
err = tx.NewSelect().
|
||||
Table("organizations").
|
||||
Column("id").
|
||||
Scan(ctx, &orgIDs)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
return err
|
||||
}
|
||||
|
||||
isPG := migration.sqlstore.BunDB().Dialect().Name() == dialect.PG
|
||||
|
||||
// zeus hosts moved from the legacy ViewAccess/AdminAccess role gates 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", "deployment-host", "list"},
|
||||
{authtypes.SigNozAdminRoleName, "metaresource", "deployment-host", "update"},
|
||||
{authtypes.SigNozEditorRoleName, "metaresource", "deployment-host", "list"},
|
||||
{authtypes.SigNozViewerRoleName, "metaresource", "deployment-host", "list"},
|
||||
}
|
||||
|
||||
for _, orgID := range orgIDs {
|
||||
for _, tuple := range tuples {
|
||||
entropy := ulid.DefaultEntropy()
|
||||
now := time.Now().UTC()
|
||||
tupleID := ulid.MustNew(ulid.Timestamp(now), entropy).String()
|
||||
|
||||
objectID := "organization/" + orgID + "/" + tuple.objectName + "/*"
|
||||
roleSubject := "organization/" + orgID + "/role/" + tuple.roleName
|
||||
|
||||
if isPG {
|
||||
user := "role:" + roleSubject + "#assignee"
|
||||
result, err := tx.ExecContext(ctx, `
|
||||
INSERT INTO tuple (store, object_type, object_id, relation, _user, user_type, ulid, inserted_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT (store, object_type, object_id, relation, _user) DO NOTHING`,
|
||||
storeID, tuple.objectType, objectID, tuple.relation, user, "userset", tupleID, now,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rowsAffected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rowsAffected == 0 {
|
||||
continue
|
||||
}
|
||||
_, err = tx.ExecContext(ctx, `
|
||||
INSERT INTO changelog (store, object_type, object_id, relation, _user, operation, ulid, inserted_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT (store, ulid, object_type) DO NOTHING`,
|
||||
storeID, tuple.objectType, objectID, tuple.relation, user, 0, tupleID, now,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
result, err := tx.ExecContext(ctx, `
|
||||
INSERT INTO tuple (store, object_type, object_id, relation, user_object_type, user_object_id, user_relation, user_type, ulid, inserted_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT (store, object_type, object_id, relation, user_object_type, user_object_id, user_relation) DO NOTHING`,
|
||||
storeID, tuple.objectType, objectID, tuple.relation, "role", roleSubject, "assignee", "userset", tupleID, now,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rowsAffected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rowsAffected == 0 {
|
||||
continue
|
||||
}
|
||||
_, err = tx.ExecContext(ctx, `
|
||||
INSERT INTO changelog (store, object_type, object_id, relation, user_object_type, user_object_id, user_relation, operation, ulid, inserted_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT (store, ulid, object_type) DO NOTHING`,
|
||||
storeID, tuple.objectType, objectID, tuple.relation, "role", roleSubject, "assignee", 0, tupleID, now,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 *addDeploymentHostTuples) Down(context.Context, *bun.DB) error {
|
||||
return nil
|
||||
}
|
||||
159
pkg/sqlmigration/119_add_license_tuples.go
Normal file
159
pkg/sqlmigration/119_add_license_tuples.go
Normal file
@@ -0,0 +1,159 @@
|
||||
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 {
|
||||
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 (migration *addLicenseTuples) Register(migrations *migrate.Migrations) error {
|
||||
return migrations.Register(migration.Up, migration.Down)
|
||||
}
|
||||
|
||||
func (migration *addLicenseTuples) Up(ctx context.Context, db *bun.DB) error {
|
||||
tx, err := db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = tx.Rollback() }()
|
||||
|
||||
var storeID string
|
||||
err = tx.QueryRowContext(ctx, `SELECT id FROM store WHERE name = ? LIMIT 1`, "signoz").Scan(&storeID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var orgIDs []string
|
||||
err = tx.NewSelect().
|
||||
Table("organizations").
|
||||
Column("id").
|
||||
Scan(ctx, &orgIDs)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
return err
|
||||
}
|
||||
|
||||
isPG := migration.sqlstore.BunDB().Dialect().Name() == dialect.PG
|
||||
|
||||
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"},
|
||||
}
|
||||
|
||||
for _, orgID := range orgIDs {
|
||||
for _, tuple := range tuples {
|
||||
entropy := ulid.DefaultEntropy()
|
||||
now := time.Now().UTC()
|
||||
tupleID := ulid.MustNew(ulid.Timestamp(now), entropy).String()
|
||||
|
||||
objectID := "organization/" + orgID + "/" + tuple.objectName + "/*"
|
||||
roleSubject := "organization/" + orgID + "/role/" + tuple.roleName
|
||||
|
||||
if isPG {
|
||||
user := "role:" + roleSubject + "#assignee"
|
||||
result, err := tx.ExecContext(ctx, `
|
||||
INSERT INTO tuple (store, object_type, object_id, relation, _user, user_type, ulid, inserted_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT (store, object_type, object_id, relation, _user) DO NOTHING`,
|
||||
storeID, tuple.objectType, objectID, tuple.relation, user, "userset", tupleID, now,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rowsAffected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rowsAffected == 0 {
|
||||
continue
|
||||
}
|
||||
_, err = tx.ExecContext(ctx, `
|
||||
INSERT INTO changelog (store, object_type, object_id, relation, _user, operation, ulid, inserted_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT (store, ulid, object_type) DO NOTHING`,
|
||||
storeID, tuple.objectType, objectID, tuple.relation, user, 0, tupleID, now,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
result, err := tx.ExecContext(ctx, `
|
||||
INSERT INTO tuple (store, object_type, object_id, relation, user_object_type, user_object_id, user_relation, user_type, ulid, inserted_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT (store, object_type, object_id, relation, user_object_type, user_object_id, user_relation) DO NOTHING`,
|
||||
storeID, tuple.objectType, objectID, tuple.relation, "role", roleSubject, "assignee", "userset", tupleID, now,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rowsAffected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rowsAffected == 0 {
|
||||
continue
|
||||
}
|
||||
_, err = tx.ExecContext(ctx, `
|
||||
INSERT INTO changelog (store, object_type, object_id, relation, user_object_type, user_object_id, user_relation, operation, ulid, inserted_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT (store, ulid, object_type) DO NOTHING`,
|
||||
storeID, tuple.objectType, objectID, tuple.relation, "role", roleSubject, "assignee", 0, tupleID, now,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
return nil
|
||||
}
|
||||
@@ -71,6 +71,7 @@ func (name Kind) Enum() []any {
|
||||
KindFactorAPIKey,
|
||||
KindLicense,
|
||||
KindSubscription,
|
||||
KindDeploymentHost,
|
||||
KindLogs,
|
||||
KindTraces,
|
||||
KindMetrics,
|
||||
|
||||
@@ -31,6 +31,7 @@ var Kinds = []Kind{
|
||||
KindFactorAPIKey,
|
||||
KindLicense,
|
||||
KindSubscription,
|
||||
KindDeploymentHost,
|
||||
KindLogs,
|
||||
KindTraces,
|
||||
KindMetrics,
|
||||
@@ -71,6 +72,7 @@ var (
|
||||
KindFactorAPIKey = MustNewKind("factor-api-key")
|
||||
KindLicense = MustNewKind("license")
|
||||
KindSubscription = MustNewKind("subscription")
|
||||
KindDeploymentHost = MustNewKind("deployment-host")
|
||||
KindLogs = MustNewKind("logs")
|
||||
KindTraces = MustNewKind("traces")
|
||||
KindMetrics = MustNewKind("metrics")
|
||||
|
||||
@@ -64,11 +64,10 @@ 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; 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.
|
||||
// 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.
|
||||
{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)},
|
||||
@@ -191,6 +190,9 @@ var ManagedRoleToTransactions = map[string][]Transaction{
|
||||
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindTTLSetting}, WildCardSelectorString)},
|
||||
{Verb: VerbUpdate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindTTLSetting}, WildCardSelectorString)},
|
||||
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindTTLSetting}, WildCardSelectorString)},
|
||||
// deployment-host — admin updates, viewer lists
|
||||
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindDeploymentHost}, WildCardSelectorString)},
|
||||
{Verb: VerbUpdate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindDeploymentHost}, WildCardSelectorString)},
|
||||
// user-preference — every authenticated user can read+update their own
|
||||
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindUserPreference}, WildCardSelectorString)},
|
||||
{Verb: VerbUpdate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindUserPreference}, WildCardSelectorString)},
|
||||
@@ -283,6 +285,8 @@ var ManagedRoleToTransactions = map[string][]Transaction{
|
||||
// ttl-setting — read only (admin updates)
|
||||
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindTTLSetting}, WildCardSelectorString)},
|
||||
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindTTLSetting}, WildCardSelectorString)},
|
||||
// deployment-host — list only (admin updates)
|
||||
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindDeploymentHost}, WildCardSelectorString)},
|
||||
// user-preference — every authenticated user can read+update their own
|
||||
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindUserPreference}, WildCardSelectorString)},
|
||||
{Verb: VerbUpdate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindUserPreference}, WildCardSelectorString)},
|
||||
@@ -341,6 +345,8 @@ var ManagedRoleToTransactions = map[string][]Transaction{
|
||||
// ttl-setting — read only
|
||||
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindTTLSetting}, WildCardSelectorString)},
|
||||
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindTTLSetting}, WildCardSelectorString)},
|
||||
// deployment-host — list only
|
||||
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindDeploymentHost}, WildCardSelectorString)},
|
||||
// user-preference — every authenticated user can read+update their own
|
||||
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindUserPreference}, WildCardSelectorString)},
|
||||
{Verb: VerbUpdate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindUserPreference}, WildCardSelectorString)},
|
||||
|
||||
@@ -31,6 +31,7 @@ var Resources = []Resource{
|
||||
ResourceMetaResourceFactorAPIKey,
|
||||
ResourceMetaResourceLicense,
|
||||
ResourceMetaResourceSubscription,
|
||||
ResourceMetaResourceDeploymentHost,
|
||||
ResourceTelemetryResourceLogs,
|
||||
ResourceTelemetryResourceTraces,
|
||||
ResourceTelemetryResourceMetrics,
|
||||
@@ -71,6 +72,7 @@ var (
|
||||
ResourceMetaResourceFactorAPIKey = NewResourceMetaResource(KindFactorAPIKey, VerbCreate, VerbList, VerbRead, VerbUpdate, VerbDelete)
|
||||
ResourceMetaResourceLicense = NewResourceMetaResource(KindLicense)
|
||||
ResourceMetaResourceSubscription = NewResourceMetaResource(KindSubscription)
|
||||
ResourceMetaResourceDeploymentHost = NewResourceMetaResource(KindDeploymentHost, VerbList, VerbUpdate)
|
||||
ResourceTelemetryResourceLogs = NewResourceTelemetryResource(KindLogs)
|
||||
ResourceTelemetryResourceTraces = NewResourceTelemetryResource(KindTraces)
|
||||
ResourceTelemetryResourceMetrics = NewResourceTelemetryResource(KindMetrics)
|
||||
|
||||
@@ -3,15 +3,19 @@ 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"`
|
||||
|
||||
@@ -28,10 +32,12 @@ type License struct {
|
||||
ID valuer.UUID
|
||||
Key string
|
||||
Data map[string]interface{}
|
||||
PlanName valuer.String
|
||||
Plan LicensePlan
|
||||
EventQueue LicenseEventQueue
|
||||
Features []*Feature
|
||||
Status valuer.String
|
||||
State string
|
||||
State valuer.String
|
||||
Platform valuer.String
|
||||
FreeUntil time.Time
|
||||
ValidFrom int64
|
||||
ValidUntil int64
|
||||
@@ -41,28 +47,51 @@ type License struct {
|
||||
OrganizationID valuer.UUID
|
||||
}
|
||||
|
||||
type GettableLicense map[string]any
|
||||
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 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{
|
||||
@@ -106,263 +135,109 @@ func GetActiveLicenseFromStorableLicenses(storableLicenses []*StorableLicense, o
|
||||
return activeLicense, nil
|
||||
}
|
||||
|
||||
func extractKeyFromMapStringInterface[T any](data map[string]interface{}, key string) (T, error) {
|
||||
var zeroValue T
|
||||
if val, ok := data[key]; ok {
|
||||
if value, ok := val.(T); ok {
|
||||
return value, nil
|
||||
}
|
||||
return zeroValue, 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")
|
||||
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")
|
||||
}
|
||||
|
||||
var features []*Feature
|
||||
|
||||
// extract id from data
|
||||
licenseIDStr, err := extractKeyFromMapStringInterface[string](licenseData, "id")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if zeusLicense.Key == "" {
|
||||
return nil, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "license key is missing")
|
||||
}
|
||||
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")
|
||||
planName, status, err := newPlanNameAndStatusFromZeusLicense(zeusLicense)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
planNameStr, err := extractKeyFromMapStringInterface[string](planMap, "name")
|
||||
features := newMergedFeatures(planName, zeusLicense.Features)
|
||||
|
||||
data, err := newDataFromZeusLicense(zeusLicense, features)
|
||||
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: licenseID,
|
||||
Key: licenseKey,
|
||||
Data: licenseData,
|
||||
PlanName: planName,
|
||||
ID: zeusLicense.ID,
|
||||
Key: zeusLicense.Key,
|
||||
Data: data,
|
||||
Plan: newLicensePlanFromZeusLicense(zeusLicense, planName),
|
||||
EventQueue: newLicenseEventQueueFromZeusLicense(zeusLicense),
|
||||
Features: features,
|
||||
ValidFrom: validFrom,
|
||||
ValidUntil: validUntil,
|
||||
ValidFrom: zeusLicense.ValidFrom,
|
||||
ValidUntil: zeusLicense.ValidUntil,
|
||||
Status: status,
|
||||
State: state,
|
||||
FreeUntil: freeUntil,
|
||||
State: valuer.NewString(zeusLicense.State),
|
||||
Platform: valuer.NewString(zeusLicense.Platform),
|
||||
FreeUntil: zeusLicense.FreeUntil,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
LastValidatedAt: time.Now(),
|
||||
OrganizationID: organizationID,
|
||||
}, nil
|
||||
|
||||
}
|
||||
|
||||
func NewLicenseFromStorableLicense(storableLicense *StorableLicense) (*License, error) {
|
||||
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")
|
||||
zeusLicense, err := NewZeusLicenseFromData(storableLicense.Data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
planNameStr, err := extractKeyFromMapStringInterface[string](planMap, "name")
|
||||
planName, status, err := newPlanNameAndStatusFromZeusLicense(zeusLicense)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
planName := valuer.NewString(planNameStr)
|
||||
// if license status is invalid then default it to basic
|
||||
if status == LicenseStatusInvalid {
|
||||
planName = PlanNameBasic
|
||||
}
|
||||
|
||||
featuresFromZeus := make([]*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
|
||||
}
|
||||
}
|
||||
}
|
||||
features := newMergedFeatures(planName, zeusLicense.Features)
|
||||
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,
|
||||
PlanName: planName,
|
||||
Plan: newLicensePlanFromZeusLicense(zeusLicense, planName),
|
||||
EventQueue: newLicenseEventQueueFromZeusLicense(zeusLicense),
|
||||
Features: features,
|
||||
ValidFrom: validFrom,
|
||||
ValidUntil: validUntil,
|
||||
ValidFrom: zeusLicense.ValidFrom,
|
||||
ValidUntil: zeusLicense.ValidUntil,
|
||||
Status: status,
|
||||
State: state,
|
||||
FreeUntil: freeUntil,
|
||||
State: valuer.NewString(zeusLicense.State),
|
||||
Platform: valuer.NewString(zeusLicense.Platform),
|
||||
FreeUntil: zeusLicense.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.PlanName.StringValue(),
|
||||
"license.state.name": license.State,
|
||||
"license.plan.name": license.Plan.Name.StringValue(),
|
||||
"license.state.name": license.State.StringValue(),
|
||||
"license.free_until.time": license.FreeUntil.UTC(),
|
||||
}
|
||||
}
|
||||
@@ -371,8 +246,8 @@ func (license *License) UpdateFeatures(features []*Feature) {
|
||||
license.Features = features
|
||||
}
|
||||
|
||||
func (license *License) Update(data []byte) error {
|
||||
updatedLicense, err := NewLicense(data, license.OrganizationID)
|
||||
func (license *License) Update(zeusLicense *zeustypes.License) error {
|
||||
updatedLicense, err := NewLicense(zeusLicense, license.OrganizationID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -382,8 +257,11 @@ func (license *License) Update(data []byte) error {
|
||||
license.Features = updatedLicense.Features
|
||||
license.ID = updatedLicense.ID
|
||||
license.Key = updatedLicense.Key
|
||||
license.PlanName = updatedLicense.PlanName
|
||||
license.Plan = updatedLicense.Plan
|
||||
license.EventQueue = updatedLicense.EventQueue
|
||||
license.Status = updatedLicense.Status
|
||||
license.State = updatedLicense.State
|
||||
license.Platform = updatedLicense.Platform
|
||||
license.ValidFrom = updatedLicense.ValidFrom
|
||||
license.ValidUntil = updatedLicense.ValidUntil
|
||||
license.UpdatedAt = currentTime
|
||||
@@ -392,13 +270,34 @@ func (license *License) Update(data []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewGettableLicense(data map[string]any, key string) *GettableLicense {
|
||||
gettableLicense := make(GettableLicense)
|
||||
for k, v := range data {
|
||||
gettableLicense[k] = v
|
||||
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),
|
||||
}
|
||||
gettableLicense["key"] = key
|
||||
return &gettableLicense
|
||||
}
|
||||
|
||||
func (p *PostableLicense) UnmarshalJSON(data []byte) error {
|
||||
@@ -424,4 +323,97 @@ 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
|
||||
}
|
||||
|
||||
@@ -1,178 +1,135 @@
|
||||
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 TestNewLicenseV3(t *testing.T) {
|
||||
func TestNewLicenseValidation(t *testing.T) {
|
||||
organizationID := valuer.MustNewUUID("0196f794-ff30-7bee-a5f4-ef5ad315715e")
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
data []byte
|
||||
pass bool
|
||||
expected *License
|
||||
error error
|
||||
name string
|
||||
data string
|
||||
errorContains string
|
||||
}{
|
||||
{
|
||||
name: "Error for missing license id",
|
||||
data: []byte(`{}`),
|
||||
pass: false,
|
||||
error: errors.New("id key is missing"),
|
||||
name: "missing license id",
|
||||
data: `{}`,
|
||||
errorContains: "license id 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 key",
|
||||
data: `{"id":"0196f794-ff30-7bee-a5f4-ef5ad315715e"}`,
|
||||
errorContains: "license key 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 status",
|
||||
data: `{"id":"0196f794-ff30-7bee-a5f4-ef5ad315715e","key":"does-not-matter"}`,
|
||||
errorContains: "license status 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"),
|
||||
},
|
||||
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",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
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)
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
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")
|
||||
}
|
||||
|
||||
@@ -17,6 +17,10 @@ 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")
|
||||
|
||||
@@ -23,6 +23,7 @@ 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 }
|
||||
@@ -45,6 +46,7 @@ func NewName(name string) (Name, error) {
|
||||
NameSpanDetailsPreviewAttributes.StringValue(),
|
||||
NameSpanDetailsColorByAttribute.StringValue(),
|
||||
NameSpanPercentileResourceAttributes.StringValue(),
|
||||
NameLogDetailsPinnedAttributes.StringValue(),
|
||||
},
|
||||
name,
|
||||
)
|
||||
|
||||
@@ -190,6 +190,15 @@ 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),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
49
pkg/types/zeustypes/license.go
Normal file
49
pkg/types/zeustypes/license.go
Normal file
@@ -0,0 +1,49 @@
|
||||
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"`
|
||||
}
|
||||
@@ -21,7 +21,7 @@ func New(_ context.Context, _ factory.ProviderSettings, _ zeus.Config) (zeus.Zeu
|
||||
return &provider{}, nil
|
||||
}
|
||||
|
||||
func (provider *provider) GetLicense(_ context.Context, _ string) ([]byte, error) {
|
||||
func (provider *provider) GetLicense(_ context.Context, _ string) (*zeustypes.License, error) {
|
||||
return nil, errors.New(errors.TypeUnsupported, zeus.ErrCodeUnsupported, "fetching license is not supported")
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ var (
|
||||
|
||||
type Zeus interface {
|
||||
// Returns the license for the given key.
|
||||
GetLicense(context.Context, string) ([]byte, error)
|
||||
GetLicense(context.Context, string) (*zeustypes.License, error)
|
||||
|
||||
// Returns the checkout URL for the given license key.
|
||||
GetCheckoutURL(context.Context, string, []byte) ([]byte, error)
|
||||
|
||||
6
tests/fixtures/auth.py
vendored
6
tests/fixtures/auth.py
vendored
@@ -189,7 +189,7 @@ def apply_license(
|
||||
request: pytest.FixtureRequest,
|
||||
pytestconfig: pytest.Config,
|
||||
) -> types.Operation:
|
||||
"""Stub Zeus license-lookup, then POST /api/v3/licenses so the BE flips
|
||||
"""Stub Zeus license-lookup, then POST /api/v4/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/v3/licenses")
|
||||
license_url = signoz.self.host_configs["8080"].get("/api/v4/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/v3/licenses"),
|
||||
url=signoz.self.host_configs["8080"].get(f"{base_path}/api/v4/licenses"),
|
||||
json={"key": "secret-key"},
|
||||
headers={"Authorization": "Bearer " + access_token},
|
||||
timeout=5,
|
||||
|
||||
5
tests/integration/testdata/alerts/test_scenarios/promql_subquery_no_step/alert_data.jsonl
vendored
Normal file
5
tests/integration/testdata/alerts/test_scenarios/promql_subquery_no_step/alert_data.jsonl
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
{"metric_name":"cpu_percent_promql_subquery_no_step","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:01:00+00:00","value":15,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_promql_subquery_no_step","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:02:00+00:00","value":15,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_promql_subquery_no_step","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:03:00+00:00","value":15,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_promql_subquery_no_step","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:04:00+00:00","value":15,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_promql_subquery_no_step","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:05:00+00:00","value":15,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
58
tests/integration/testdata/alerts/test_scenarios/promql_subquery_no_step/rule.json
vendored
Normal file
58
tests/integration/testdata/alerts/test_scenarios/promql_subquery_no_step/rule.json
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"alert": "promql_subquery_no_step",
|
||||
"ruleType": "promql_rule",
|
||||
"alertType": "METRIC_BASED_ALERT",
|
||||
"condition": {
|
||||
"thresholds": {
|
||||
"kind": "basic",
|
||||
"spec": [
|
||||
{
|
||||
"name": "critical",
|
||||
"target": 10,
|
||||
"matchType": "at_least_once",
|
||||
"op": "above",
|
||||
"channels": [
|
||||
"test channel"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"compositeQuery": {
|
||||
"queryType": "promql",
|
||||
"panelType": "graph",
|
||||
"queries": [
|
||||
{
|
||||
"type": "promql",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"query": "max_over_time({\"cpu_percent_promql_subquery_no_step\"}[2m:])"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"selectedQueryName": "A"
|
||||
},
|
||||
"evaluation": {
|
||||
"kind": "rolling",
|
||||
"spec": {
|
||||
"evalWindow": "5m0s",
|
||||
"frequency": "15s"
|
||||
}
|
||||
},
|
||||
"labels": {},
|
||||
"annotations": {
|
||||
"description": "This alert is fired when the defined metric (current value: {{$value}}) crosses the threshold ({{$threshold}})",
|
||||
"summary": "This alert is fired when the defined metric (current value: {{$value}}) crosses the threshold ({{$threshold}})"
|
||||
},
|
||||
"notificationSettings": {
|
||||
"groupBy": [],
|
||||
"usePolicy": false,
|
||||
"renotify": {
|
||||
"enabled": false,
|
||||
"interval": "30m",
|
||||
"alertStates": []
|
||||
}
|
||||
},
|
||||
"version": "v5",
|
||||
"schemaVersion": "v2alpha1"
|
||||
}
|
||||
@@ -295,6 +295,14 @@
|
||||
"list"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "metaresource",
|
||||
"kind": "deployment-host",
|
||||
"verbs": [
|
||||
"list",
|
||||
"update"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "metaresource",
|
||||
"kind": "user-preference",
|
||||
@@ -498,6 +506,13 @@
|
||||
"list"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "metaresource",
|
||||
"kind": "deployment-host",
|
||||
"verbs": [
|
||||
"list"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "metaresource",
|
||||
"kind": "user-preference",
|
||||
@@ -654,6 +669,13 @@
|
||||
"list"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "metaresource",
|
||||
"kind": "deployment-host",
|
||||
"verbs": [
|
||||
"list"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "metaresource",
|
||||
"kind": "user-preference",
|
||||
|
||||
93
tests/integration/tests/alerts/04_promql_subquery_no_step.py
Normal file
93
tests/integration/tests/alerts/04_promql_subquery_no_step.py
Normal file
@@ -0,0 +1,93 @@
|
||||
import json
|
||||
import uuid
|
||||
from collections.abc import Callable
|
||||
from datetime import UTC, datetime, timedelta
|
||||
|
||||
from wiremock.client import HttpMethods, Mapping, MappingRequest, MappingResponse
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.alerts import (
|
||||
update_rule_channel_name,
|
||||
verify_webhook_alert_expectation,
|
||||
)
|
||||
from fixtures.fs import get_testdata_file_path
|
||||
|
||||
TEST_CASE = types.AlertTestCase(
|
||||
name="promql_subquery_no_step",
|
||||
rule_path="alerts/test_scenarios/promql_subquery_no_step/rule.json",
|
||||
alert_data=[
|
||||
types.AlertData(
|
||||
type="metrics",
|
||||
data_path="alerts/test_scenarios/promql_subquery_no_step/alert_data.jsonl",
|
||||
),
|
||||
],
|
||||
alert_expectation=types.AlertExpectation(
|
||||
should_alert=True,
|
||||
wait_time_seconds=30,
|
||||
expected_alerts=[
|
||||
types.FiringAlert(
|
||||
labels={
|
||||
"alertname": "promql_subquery_no_step",
|
||||
"threshold.name": "critical",
|
||||
}
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def test_promql_rule_subquery_without_step(
|
||||
notification_channel: types.TestContainerDocker,
|
||||
make_http_mocks: Callable[[types.TestContainerDocker, list[Mapping]], None],
|
||||
create_webhook_notification_channel: Callable[[str, str, dict, bool], str],
|
||||
create_alert_rule: Callable[[dict], str],
|
||||
insert_alert_data: Callable[[list[types.AlertData], datetime], None],
|
||||
):
|
||||
"""
|
||||
A promql rule with a step-less subquery ([2m:]) must evaluate and fire.
|
||||
A nil NoStepSubqueryIntervalFn segfaults the process on first evaluation.
|
||||
"""
|
||||
notification_channel_name = str(uuid.uuid4())
|
||||
webhook_endpoint_path = f"/alert/{notification_channel_name}"
|
||||
notification_url = notification_channel.container_configs["8080"].get(webhook_endpoint_path)
|
||||
|
||||
make_http_mocks(
|
||||
notification_channel,
|
||||
[
|
||||
Mapping(
|
||||
request=MappingRequest(
|
||||
method=HttpMethods.POST,
|
||||
url=webhook_endpoint_path,
|
||||
),
|
||||
response=MappingResponse(
|
||||
status=200,
|
||||
json_body={},
|
||||
),
|
||||
persistent=False,
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
create_webhook_notification_channel(
|
||||
channel_name=notification_channel_name,
|
||||
webhook_url=notification_url,
|
||||
http_config={},
|
||||
send_resolved=False,
|
||||
)
|
||||
|
||||
insert_alert_data(
|
||||
TEST_CASE.alert_data,
|
||||
base_time=datetime.now(tz=UTC) - timedelta(minutes=5),
|
||||
)
|
||||
|
||||
rule_path = get_testdata_file_path(TEST_CASE.rule_path)
|
||||
with open(rule_path, encoding="utf-8") as f:
|
||||
rule_data = json.loads(f.read())
|
||||
update_rule_channel_name(rule_data, notification_channel_name)
|
||||
create_alert_rule(rule_data)
|
||||
|
||||
verify_webhook_alert_expectation(
|
||||
notification_channel,
|
||||
notification_channel_name,
|
||||
TEST_CASE.alert_expectation,
|
||||
)
|
||||
@@ -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/v3/licenses"),
|
||||
url=signoz.self.host_configs["8080"].get("/api/v4/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/v3/licenses"),
|
||||
url=signoz.self.host_configs["8080"].get("/api/v4/licenses/0196360e-90cd-7a74-8313-1aa815ce2a67"),
|
||||
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/v3/licenses/active"),
|
||||
url=signoz.self.host_configs["8080"].get("/api/v4/licenses/active"),
|
||||
headers={"Authorization": "Bearer " + access_token},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == http.HTTPStatus.OK
|
||||
assert response.json()["data"]["valid_from"] == 1732146922
|
||||
assert response.json()["data"]["validFrom"] == 1732146922
|
||||
|
||||
response = requests.post(
|
||||
url=signoz.zeus.host_configs["8080"].get("/__admin/requests/count"),
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
from collections.abc import Callable
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from http import HTTPStatus
|
||||
from uuid import uuid4
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
|
||||
from fixtures.metrics import Metrics
|
||||
from fixtures.querier import get_all_series, make_query_request
|
||||
|
||||
MINUTE_MS = 60_000
|
||||
|
||||
LEGS: list[tuple[str, dict | None]] = [
|
||||
("default", None),
|
||||
("clickhousev2", {"X-SigNoz-PromQL-Provider": "clickhousev2"}),
|
||||
]
|
||||
|
||||
|
||||
def test_promql_subquery_without_step_evaluates(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_metrics: Callable[[list[Metrics]], None],
|
||||
) -> None:
|
||||
"""
|
||||
A subquery that omits its step, e.g. `metric[5m:]`, is valid PromQL: the
|
||||
engine fills in its default resolution. A nil NoStepSubqueryIntervalFn
|
||||
segfaults the whole process on the first such query.
|
||||
"""
|
||||
end_ms = (int((datetime.now(tz=UTC) - timedelta(minutes=5)).timestamp() * 1000) // MINUTE_MS) * MINUTE_MS
|
||||
start_ms = end_ms - 30 * MINUTE_MS
|
||||
|
||||
metric = f"no_step_subquery_gauge_{uuid4().hex[:8]}"
|
||||
insert_metrics(
|
||||
[
|
||||
Metrics(
|
||||
metric_name=metric,
|
||||
labels={"host": "server-01"},
|
||||
timestamp=datetime.fromtimestamp(ts_ms / 1000, tz=UTC),
|
||||
value=42.0,
|
||||
)
|
||||
for ts_ms in range(start_ms, end_ms + 1, MINUTE_MS)
|
||||
]
|
||||
)
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
for leg, headers in LEGS:
|
||||
query = {"type": "promql", "spec": {"name": "A", "query": f"max_over_time({metric}[5m:])"}}
|
||||
response = make_query_request(signoz, token, start_ms, end_ms, [query], headers=headers)
|
||||
assert response.status_code == HTTPStatus.OK, f"{leg}: {response.text[:300]}"
|
||||
series = get_all_series(response.json(), "A")
|
||||
assert series, f"{leg}: the subquery must return the inserted series"
|
||||
values = {point["value"] for entry in series for point in entry.get("values") or []}
|
||||
assert values == {42.0}, f"{leg}: {sorted(values)[:5]}"
|
||||
|
||||
# A plain follow-up query proves the process survived the subquery legs.
|
||||
response = make_query_request(
|
||||
signoz,
|
||||
token,
|
||||
start_ms,
|
||||
end_ms,
|
||||
[{"type": "promql", "spec": {"name": "A", "query": metric}}],
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text[:300]
|
||||
Reference in New Issue
Block a user