mirror of
https://github.com/SigNoz/signoz.git
synced 2026-08-31 16:40:44 +01:00
Compare commits
14 Commits
issue_5975
...
feat/licen
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c0dce31ed | ||
|
|
f65c27c236 | ||
|
|
405e92cd06 | ||
|
|
ce3966b8c4 | ||
|
|
f2ce56de35 | ||
|
|
5e06339afe | ||
|
|
32f9254035 | ||
|
|
764fe8ec69 | ||
|
|
ea7fb4cdaa | ||
|
|
da9b4644df | ||
|
|
095821264e | ||
|
|
5069bf80b0 | ||
|
|
1086914a3b | ||
|
|
3e2daaea0e |
@@ -2559,6 +2559,7 @@ components:
|
||||
- factor-api-key
|
||||
- license
|
||||
- subscription
|
||||
- deployment-host
|
||||
- logs
|
||||
- traces
|
||||
- metrics
|
||||
@@ -5641,6 +5642,171 @@ 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
|
||||
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:
|
||||
@@ -9222,10 +9388,6 @@ paths:
|
||||
name: name
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: existingQuery
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
@@ -23946,9 +24108,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
|
||||
@@ -24002,9 +24164,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
|
||||
@@ -24203,6 +24365,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/orgs/me/license:
|
||||
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/LicensetypesGettableLicense'
|
||||
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) {
|
||||
|
||||
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/orgs/me/license`,
|
||||
method: 'GET',
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getGetActiveLicenseQueryKey = () => {
|
||||
return [`/api/v4/orgs/me/license`] 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,197 @@ 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 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
|
||||
*/
|
||||
@@ -10248,11 +10440,6 @@ export type GetAIObservabilityFieldsValuesParams = {
|
||||
* @description undefined
|
||||
*/
|
||||
name?: string;
|
||||
/**
|
||||
* @type string
|
||||
* @description undefined
|
||||
*/
|
||||
existingQuery?: string;
|
||||
};
|
||||
|
||||
export type GetAIObservabilityFieldsValues200 = {
|
||||
@@ -12433,6 +12620,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: LicensetypesGettableLicenseDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type GetWaterfallV4PathParameters = {
|
||||
traceID: string;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
}));
|
||||
};
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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],
|
||||
);
|
||||
};
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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",
|
||||
|
||||
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/orgs/me/license", 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.GettableLicense),
|
||||
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.NewGettableLicense(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")
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"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/modules/aiobservability"
|
||||
@@ -65,6 +66,13 @@ func (handler *handler) GetFieldsValues(rw http.ResponseWriter, req *http.Reques
|
||||
ctx, cancel := context.WithTimeout(req.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// binding ignores query params the struct does not declare, so an unsupported
|
||||
// existingQuery would silently return values it did not narrow
|
||||
if req.URL.Query().Has("existingQuery") {
|
||||
render.Error(rw, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "existingQuery is not supported"))
|
||||
return
|
||||
}
|
||||
|
||||
var params aiobservabilitytypes.PostableFieldValueParams
|
||||
if err := binding.Query.BindQuery(req.URL.Query(), ¶ms); err != nil {
|
||||
render.Error(rw, err)
|
||||
@@ -76,29 +84,18 @@ func (handler *handler) GetFieldsValues(rw http.ResponseWriter, req *http.Reques
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
orgID := valuer.MustNewUUID(claims.OrgID)
|
||||
|
||||
params.ExistingQuery = aitelemetryschema.ScopedExistingQuery(params.ExistingQuery)
|
||||
fieldValueSelector := aiobservabilitytypes.NewFieldValueSelectorFromPostableFieldValueParams(params)
|
||||
|
||||
values := &telemetrytypes.TelemetryFieldValues{}
|
||||
complete := true
|
||||
// the trace context names the computed per-trace aggregates, which are never ingested
|
||||
if fieldValueSelector.FieldContext != telemetrytypes.FieldContextTrace {
|
||||
values, complete, err = handler.telemetryMetadataStore.GetAllValues(ctx, orgID, fieldValueSelector)
|
||||
values, complete, err = handler.telemetryMetadataStore.GetAllValues(ctx, valuer.MustNewUUID(claims.OrgID), fieldValueSelector)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
// related values are best-effort: on failure the plain values still serve
|
||||
// the filter bar
|
||||
relatedValues, relatedComplete, err := handler.telemetryMetadataStore.GetRelatedValues(ctx, orgID, fieldValueSelector)
|
||||
if err != nil {
|
||||
relatedValues = []string{}
|
||||
}
|
||||
values.RelatedValues = relatedValues
|
||||
complete = complete && relatedComplete
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusOK, &telemetrytypes.GettableFieldValues{
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package aistatementbuilder
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/flagger"
|
||||
"github.com/SigNoz/signoz/pkg/statementbuilder"
|
||||
@@ -25,8 +27,11 @@ func NewFactory(
|
||||
// Scope describes gen_ai for the scoped trace builder: an AI trace has >=1 gen_ai
|
||||
// LLM, tool, or agent span, and its list adds AI/LLM per-trace metrics.
|
||||
func Scope() scopedtraces.TraceScope {
|
||||
gateKeys := make([]*telemetrytypes.TelemetryFieldKey, 0, len(aiobservabilitytypes.GenAISpanGateKeys))
|
||||
for _, name := range aiobservabilitytypes.GenAISpanGateKeys {
|
||||
gateKeyNames := []string{aiobservabilitytypes.GenAIRequestModel, aiobservabilitytypes.GenAIToolName, aiobservabilitytypes.GenAIAgentName}
|
||||
gateExprs := make([]string, 0, len(gateKeyNames))
|
||||
gateKeys := make([]*telemetrytypes.TelemetryFieldKey, 0, len(gateKeyNames))
|
||||
for _, name := range gateKeyNames {
|
||||
gateExprs = append(gateExprs, name+" EXISTS")
|
||||
gateKeys = append(gateKeys, &telemetrytypes.TelemetryFieldKey{
|
||||
Name: name,
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
@@ -74,7 +79,7 @@ func Scope() scopedtraces.TraceScope {
|
||||
}
|
||||
|
||||
return scopedtraces.TraceScope{
|
||||
FilterExpression: aiobservabilitytypes.GenAISpanFilterExpression(),
|
||||
FilterExpression: strings.Join(gateExprs, " OR "),
|
||||
FieldKeys: gateKeys,
|
||||
Columns: columns,
|
||||
DefaultOrderAlias: "last_activity_time",
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
package aitelemetryschema
|
||||
|
||||
import (
|
||||
"github.com/SigNoz/signoz/pkg/querybuilder"
|
||||
"github.com/SigNoz/signoz/pkg/types/aiobservabilitytypes"
|
||||
)
|
||||
|
||||
var (
|
||||
traceAggregateNames = func() map[string]struct{} {
|
||||
names := make(map[string]struct{}, len(TraceAggregateFields))
|
||||
for name := range TraceAggregateFields {
|
||||
names[name] = struct{}{}
|
||||
}
|
||||
return names
|
||||
}()
|
||||
|
||||
genAISpanGate = "(" + aiobservabilitytypes.GenAISpanFilterExpression() + ")"
|
||||
)
|
||||
|
||||
// ScopedExistingQuery narrows value suggestions to gen_ai spans: the caller's
|
||||
// filter minus its per-trace aggregate atoms (never ingested, so nothing can
|
||||
// narrow on them), ANDed with the gen_ai span gate. An unparseable filter is
|
||||
// dropped, matching how the metadata store treats it downstream.
|
||||
func ScopedExistingQuery(existingQuery string) string {
|
||||
spanExpr := ""
|
||||
if existingQuery != "" {
|
||||
if expr, _, err := querybuilder.SplitFilterForAggregates(existingQuery, traceAggregateNames); err == nil {
|
||||
spanExpr = expr
|
||||
}
|
||||
}
|
||||
|
||||
if spanExpr == "" {
|
||||
return genAISpanGate
|
||||
}
|
||||
return genAISpanGate + " AND (" + spanExpr + ")"
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
package aitelemetryschema
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestScopedExistingQuery(t *testing.T) {
|
||||
gate := "(gen_ai.request.model EXISTS OR gen_ai.tool.name EXISTS OR gen_ai.agent.name EXISTS)"
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
existingQuery string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "empty query returns the gate alone",
|
||||
existingQuery: "",
|
||||
expected: gate,
|
||||
},
|
||||
{
|
||||
name: "span filter is preserved under the gate",
|
||||
existingQuery: "service.name = 'checkout'",
|
||||
expected: gate + " AND (service.name = 'checkout')",
|
||||
},
|
||||
{
|
||||
name: "trace aggregate filter is stripped",
|
||||
existingQuery: "llm_call_count > 5",
|
||||
expected: gate,
|
||||
},
|
||||
{
|
||||
name: "mixed filter keeps only the span part",
|
||||
existingQuery: "llm_call_count > 5 AND gen_ai.request.model = 'gpt-4'",
|
||||
expected: gate + " AND (gen_ai.request.model = 'gpt-4')",
|
||||
},
|
||||
{
|
||||
name: "trace context filter is stripped",
|
||||
existingQuery: "trace.total_tokens > 100 AND service.name = 'checkout'",
|
||||
expected: gate + " AND (service.name = 'checkout')",
|
||||
},
|
||||
{
|
||||
name: "unparseable filter is dropped",
|
||||
existingQuery: "service.name = ",
|
||||
expected: gate,
|
||||
},
|
||||
{
|
||||
name: "multiple span conditions survive as one AND chain",
|
||||
existingQuery: "service.name = 'checkout' AND gen_ai.request.model = 'gpt-4' AND llm_call_count > 5",
|
||||
expected: gate + " AND (service.name = 'checkout' AND gen_ai.request.model = 'gpt-4')",
|
||||
},
|
||||
{
|
||||
name: "span OR group is kept whole and parenthesized against the AND join",
|
||||
existingQuery: "service.name = 'a' OR service.name = 'b'",
|
||||
expected: gate + " AND ((service.name = 'a' OR service.name = 'b'))",
|
||||
},
|
||||
{
|
||||
name: "parenthesized span OR group ANDed with an aggregate keeps only the group",
|
||||
existingQuery: "(service.name = 'a' OR service.name = 'b') AND llm_call_count > 5",
|
||||
expected: gate + " AND ((service.name = 'a' OR service.name = 'b'))",
|
||||
},
|
||||
{
|
||||
name: "OR group of trace aggregates is stripped whole",
|
||||
existingQuery: "llm_call_count > 5 OR total_tokens > 100",
|
||||
expected: gate,
|
||||
},
|
||||
{
|
||||
name: "OR mixing aggregate and span atoms drops the whole filter",
|
||||
existingQuery: "llm_call_count > 5 OR service.name = 'checkout'",
|
||||
expected: gate,
|
||||
},
|
||||
{
|
||||
name: "parenthesized AND group is split, not routed whole",
|
||||
existingQuery: "(llm_call_count > 5 AND service.name = 'checkout') AND gen_ai.request.model = 'gpt-4'",
|
||||
expected: gate + " AND (service.name = 'checkout' AND gen_ai.request.model = 'gpt-4')",
|
||||
},
|
||||
{
|
||||
name: "NOT over an aggregate group is stripped",
|
||||
existingQuery: "NOT (llm_call_count > 5) AND service.name = 'checkout'",
|
||||
expected: gate + " AND (service.name = 'checkout')",
|
||||
},
|
||||
{
|
||||
name: "NOT over a span group is kept",
|
||||
existingQuery: "NOT (service.name = 'checkout')",
|
||||
expected: gate + " AND (NOT (service.name = 'checkout'))",
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
assert.Equal(t, testCase.expected, ScopedExistingQuery(testCase.existingQuery))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -15,12 +15,11 @@ type PostableFieldKeysParams struct {
|
||||
Limit int `query:"limit"`
|
||||
}
|
||||
|
||||
// existingQuery may reference the computed per-trace aggregates, which are
|
||||
// never ingested; those filters are stripped before narrowing values.
|
||||
// existingQuery is unsupported until the computed per-trace aggregates it may
|
||||
// reference can be narrowed on.
|
||||
type PostableFieldValueParams struct {
|
||||
PostableFieldKeysParams
|
||||
Name string `query:"name"`
|
||||
ExistingQuery string `query:"existingQuery"`
|
||||
Name string `query:"name"`
|
||||
}
|
||||
|
||||
func NewFieldKeySelectorFromPostableFieldKeysParams(params PostableFieldKeysParams) *telemetrytypes.FieldKeySelector {
|
||||
@@ -31,7 +30,6 @@ func NewFieldValueSelectorFromPostableFieldValueParams(params PostableFieldValue
|
||||
return telemetrytypes.NewFieldValueSelectorFromPostableFieldValueParams(telemetrytypes.PostableFieldValueParams{
|
||||
PostableFieldKeysParams: params.telemetryParams(),
|
||||
Name: params.Name,
|
||||
ExistingQuery: params.ExistingQuery,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package aiobservabilitytypes
|
||||
|
||||
import "strings"
|
||||
|
||||
// OpenTelemetry gen_ai semantic-convention attribute keys. Single source of truth
|
||||
// shared by the AI query builder and the LLM pricing pipeline.
|
||||
const (
|
||||
@@ -20,20 +18,6 @@ const (
|
||||
GenAIOutputMessages = "gen_ai.output.messages"
|
||||
)
|
||||
|
||||
// GenAISpanGateKeys mark a span as gen_ai: an LLM call, a tool call, or an
|
||||
// agent span. A trace belongs to the AI explorer when any span carries one.
|
||||
var GenAISpanGateKeys = []string{GenAIRequestModel, GenAIToolName, GenAIAgentName}
|
||||
|
||||
// GenAISpanFilterExpression renders the gate as a query-builder filter
|
||||
// expression: each gate key ORed on EXISTS.
|
||||
func GenAISpanFilterExpression() string {
|
||||
exprs := make([]string, 0, len(GenAISpanGateKeys))
|
||||
for _, key := range GenAISpanGateKeys {
|
||||
exprs = append(exprs, key+" EXISTS")
|
||||
}
|
||||
return strings.Join(exprs, " OR ")
|
||||
}
|
||||
|
||||
// Per-span costs the SigNoz LLM pricing processor attaches; not OTel semconv.
|
||||
const (
|
||||
SignozGenAICostInput = "_signoz.gen_ai.cost_input"
|
||||
|
||||
@@ -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/orgs/me/license 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,47 @@ 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 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 +131,201 @@ 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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// 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 +334,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 +345,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 +358,28 @@ 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,
|
||||
}
|
||||
gettableLicense["key"] = key
|
||||
return &gettableLicense
|
||||
}
|
||||
|
||||
func (p *PostableLicense) UnmarshalJSON(data []byte) error {
|
||||
@@ -424,4 +405,5 @@ 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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
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,
|
||||
)
|
||||
@@ -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]
|
||||
@@ -2,12 +2,10 @@ from collections.abc import Callable
|
||||
from datetime import UTC, datetime
|
||||
from http import HTTPStatus
|
||||
|
||||
import pytest
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
|
||||
from fixtures.metadata import AttributesMetadata, get_field_keys, get_field_values
|
||||
from fixtures.querierai import ai_trace, ai_trace_mixed_spans
|
||||
from fixtures.metadata import get_field_keys, get_field_values
|
||||
from fixtures.querierai import ai_trace
|
||||
from fixtures.traces import Traces
|
||||
|
||||
AI_KEYS_PATH = "/api/v1/ai_observability/fields/keys"
|
||||
@@ -108,94 +106,20 @@ def test_ai_field_values_suggests_ingested_attribute_values(
|
||||
assert values["stringValues"] == ["gpt-it-values"], values
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"existing_query,search_text,expected",
|
||||
[
|
||||
pytest.param(None, "", {"ai-rel-a", "ai-rel-b", "ai-rel-c"}, id="no_query_scopes_to_gen_ai_spans"),
|
||||
pytest.param("gen_ai.user.id = 'alice'", "", {"ai-rel-a"}, id="span_filter_narrows_under_the_gate"),
|
||||
pytest.param("llm_call_count > 0", "", {"ai-rel-a", "ai-rel-b", "ai-rel-c"}, id="pure_trace_aggregate_filter_is_stripped"),
|
||||
pytest.param("llm_call_count > 0 AND gen_ai.user.id = 'alice'", "", {"ai-rel-a"}, id="mixed_filter_keeps_only_the_span_part"),
|
||||
pytest.param(
|
||||
"llm_call_count > 0 OR gen_ai.user.id = 'alice'",
|
||||
"",
|
||||
{"ai-rel-a", "ai-rel-b", "ai-rel-c"},
|
||||
id="class_mixing_or_drops_the_filter_not_the_request",
|
||||
),
|
||||
pytest.param(
|
||||
"gen_ai.user.id = ",
|
||||
"",
|
||||
{"ai-rel-a", "ai-rel-b", "ai-rel-c"},
|
||||
id="unparseable_filter_falls_back_to_the_gate",
|
||||
),
|
||||
pytest.param(None, "ai-rel-a", {"ai-rel-a"}, id="search_text_narrows_related_values"),
|
||||
# http.request.method lives on the root span's metadata row, gen_ai.* on
|
||||
# the LLM/tool/agent rows; rows are per span-shape, so the gate AND a
|
||||
# cross-span attribute filter can match no single row
|
||||
pytest.param("http.request.method = 'POST'", "", set(), id="cross_span_attribute_filter_matches_no_row"),
|
||||
],
|
||||
)
|
||||
def test_ai_field_values_related_values(
|
||||
def test_ai_field_values_reject_existing_query(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_traces: Callable[[list[Traces]], None],
|
||||
insert_attributes_metadata: Callable[[list[AttributesMetadata]], None],
|
||||
existing_query: str | None,
|
||||
search_text: str,
|
||||
expected: set[str],
|
||||
) -> None:
|
||||
now = datetime.now(tz=UTC).replace(second=0, microsecond=0)
|
||||
|
||||
# existingQuery key resolution reads the trace keys tables, not
|
||||
# attributes_metadata; a mixed trace registers the gate keys (model/tool/
|
||||
# agent) plus gen_ai.user.id and http.request.method
|
||||
insert_traces(ai_trace_mixed_spans(now=now, service="ai-rel-a", user="alice"))
|
||||
|
||||
# related values are served from attributes_metadata; one row per gate key,
|
||||
# the traces row without any gate attribute and the logs row (wrong
|
||||
# data_source, gate attribute present) must never surface
|
||||
insert_attributes_metadata(
|
||||
[
|
||||
AttributesMetadata(
|
||||
data_source="traces",
|
||||
resource_attributes={"service.name": "ai-rel-a"},
|
||||
attributes={"gen_ai.request.model": "gpt-rel", "gen_ai.user.id": "alice"},
|
||||
),
|
||||
AttributesMetadata(
|
||||
data_source="traces",
|
||||
resource_attributes={"service.name": "ai-rel-b"},
|
||||
attributes={"gen_ai.tool.name": "get_weather", "gen_ai.user.id": "bob"},
|
||||
),
|
||||
AttributesMetadata(
|
||||
data_source="traces",
|
||||
resource_attributes={"service.name": "ai-rel-c"},
|
||||
attributes={"gen_ai.agent.name": "chat-agent"},
|
||||
),
|
||||
AttributesMetadata(
|
||||
data_source="traces",
|
||||
resource_attributes={"service.name": "plain-rel"},
|
||||
attributes={"http.request.method": "POST"},
|
||||
),
|
||||
AttributesMetadata(
|
||||
data_source="logs",
|
||||
resource_attributes={"service.name": "ai-rel-logs"},
|
||||
attributes={"gen_ai.request.model": "gpt-rel"},
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
params = {"name": "service.name", "searchText": search_text}
|
||||
if existing_query is not None:
|
||||
params["existingQuery"] = existing_query
|
||||
|
||||
response = get_field_values(signoz, token, params, AI_VALUES_PATH)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
assert response.json()["status"] == "success"
|
||||
|
||||
related = response.json()["data"]["values"].get("relatedValues") or []
|
||||
assert set(related) == expected, related
|
||||
response = get_field_values(
|
||||
signoz,
|
||||
token,
|
||||
{"name": "gen_ai.request.model", "existingQuery": "service.name = 'ai-it-values'"},
|
||||
AI_VALUES_PATH,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.BAD_REQUEST, response.text
|
||||
|
||||
|
||||
def test_ai_field_values_of_computed_aggregate_are_empty(
|
||||
|
||||
Reference in New Issue
Block a user