mirror of
https://github.com/SigNoz/signoz.git
synced 2026-09-22 19:30:43 +01:00
Compare commits
11 Commits
feat/alert
...
bottom-str
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a7f19cc3d | ||
|
|
7d7200ead6 | ||
|
|
13a57ebb9c | ||
|
|
733a1fbb73 | ||
|
|
9f20158225 | ||
|
|
9833797abe | ||
|
|
52995b252e | ||
|
|
deb7854b44 | ||
|
|
f7c47408e9 | ||
|
|
1af45169d5 | ||
|
|
0c2a874e07 |
@@ -3454,29 +3454,6 @@ components:
|
||||
required:
|
||||
- customValue
|
||||
type: object
|
||||
DashboardtypesDashboard:
|
||||
properties:
|
||||
createdAt:
|
||||
format: date-time
|
||||
type: string
|
||||
createdBy:
|
||||
type: string
|
||||
data:
|
||||
$ref: '#/components/schemas/DashboardtypesStorableDashboardData'
|
||||
id:
|
||||
type: string
|
||||
locked:
|
||||
type: boolean
|
||||
org_id:
|
||||
type: string
|
||||
source:
|
||||
$ref: '#/components/schemas/DashboardtypesSource'
|
||||
updatedAt:
|
||||
format: date-time
|
||||
type: string
|
||||
updatedBy:
|
||||
type: string
|
||||
type: object
|
||||
DashboardtypesDashboardPanelRef:
|
||||
properties:
|
||||
dashboardId:
|
||||
@@ -3656,13 +3633,6 @@ components:
|
||||
timeRangeEnabled:
|
||||
type: boolean
|
||||
type: object
|
||||
DashboardtypesGettablePublicDashboardData:
|
||||
properties:
|
||||
dashboard:
|
||||
$ref: '#/components/schemas/DashboardtypesDashboard'
|
||||
publicDashboard:
|
||||
$ref: '#/components/schemas/DashboardtypesGettablePublicDasbhboard'
|
||||
type: object
|
||||
DashboardtypesGettablePublicDashboardDataV2:
|
||||
properties:
|
||||
dashboard:
|
||||
@@ -4447,9 +4417,6 @@ components:
|
||||
- normal
|
||||
- percent
|
||||
type: string
|
||||
DashboardtypesStorableDashboardData:
|
||||
additionalProperties: {}
|
||||
type: object
|
||||
DashboardtypesTableFormatting:
|
||||
properties:
|
||||
columnUnits:
|
||||
@@ -7225,22 +7192,6 @@ components:
|
||||
- attributes
|
||||
- totalKeys
|
||||
type: object
|
||||
MetricsexplorertypesMetricDashboard:
|
||||
properties:
|
||||
dashboardId:
|
||||
type: string
|
||||
dashboardName:
|
||||
type: string
|
||||
widgetId:
|
||||
type: string
|
||||
widgetName:
|
||||
type: string
|
||||
required:
|
||||
- dashboardName
|
||||
- dashboardId
|
||||
- widgetId
|
||||
- widgetName
|
||||
type: object
|
||||
MetricsexplorertypesMetricDashboardPanelsResponse:
|
||||
properties:
|
||||
dashboards:
|
||||
@@ -7251,16 +7202,6 @@ components:
|
||||
required:
|
||||
- dashboards
|
||||
type: object
|
||||
MetricsexplorertypesMetricDashboardsResponse:
|
||||
properties:
|
||||
dashboards:
|
||||
items:
|
||||
$ref: '#/components/schemas/MetricsexplorertypesMetricDashboard'
|
||||
nullable: true
|
||||
type: array
|
||||
required:
|
||||
- dashboards
|
||||
type: object
|
||||
MetricsexplorertypesMetricHighlightsResponse:
|
||||
properties:
|
||||
activeTimeSeries:
|
||||
@@ -8994,98 +8935,6 @@ components:
|
||||
message:
|
||||
type: string
|
||||
type: object
|
||||
RuletypesLabelPair:
|
||||
properties:
|
||||
key:
|
||||
type: string
|
||||
value:
|
||||
type: string
|
||||
required:
|
||||
- key
|
||||
- value
|
||||
type: object
|
||||
RuletypesListOrder:
|
||||
enum:
|
||||
- asc
|
||||
- desc
|
||||
type: string
|
||||
RuletypesListSort:
|
||||
enum:
|
||||
- updated_at
|
||||
- created_at
|
||||
- name
|
||||
- state
|
||||
- severity
|
||||
type: string
|
||||
RuletypesListableRule:
|
||||
properties:
|
||||
alert:
|
||||
type: string
|
||||
alertType:
|
||||
$ref: '#/components/schemas/RuletypesAlertType'
|
||||
createdAt:
|
||||
format: date-time
|
||||
type: string
|
||||
createdBy:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
disabled:
|
||||
type: boolean
|
||||
id:
|
||||
type: string
|
||||
labels:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
ruleType:
|
||||
$ref: '#/components/schemas/RuletypesRuleType'
|
||||
state:
|
||||
$ref: '#/components/schemas/RuletypesAlertState'
|
||||
updatedAt:
|
||||
format: date-time
|
||||
type: string
|
||||
updatedBy:
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
- state
|
||||
- alert
|
||||
- alertType
|
||||
- ruleType
|
||||
type: object
|
||||
RuletypesListableRuleViews:
|
||||
properties:
|
||||
views:
|
||||
items:
|
||||
$ref: '#/components/schemas/RuletypesRuleView'
|
||||
type: array
|
||||
required:
|
||||
- views
|
||||
type: object
|
||||
RuletypesListableRules:
|
||||
properties:
|
||||
labels:
|
||||
items:
|
||||
$ref: '#/components/schemas/RuletypesLabelPair'
|
||||
type: array
|
||||
reservedKeywords:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
rules:
|
||||
items:
|
||||
$ref: '#/components/schemas/RuletypesListableRule'
|
||||
type: array
|
||||
total:
|
||||
format: int64
|
||||
type: integer
|
||||
required:
|
||||
- rules
|
||||
- total
|
||||
- labels
|
||||
- reservedKeywords
|
||||
type: object
|
||||
RuletypesMatchType:
|
||||
enum:
|
||||
- at_least_once
|
||||
@@ -9160,16 +9009,6 @@ components:
|
||||
- ruleType
|
||||
- condition
|
||||
type: object
|
||||
RuletypesPostableRuleView:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/RuletypesRuleViewData'
|
||||
name:
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
- data
|
||||
type: object
|
||||
RuletypesQueryType:
|
||||
enum:
|
||||
- builder
|
||||
@@ -9308,45 +9147,6 @@ components:
|
||||
- promql_rule
|
||||
- anomaly_rule
|
||||
type: string
|
||||
RuletypesRuleView:
|
||||
properties:
|
||||
createdAt:
|
||||
format: date-time
|
||||
type: string
|
||||
data:
|
||||
$ref: '#/components/schemas/RuletypesRuleViewData'
|
||||
id:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
orgId:
|
||||
type: string
|
||||
updatedAt:
|
||||
format: date-time
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
- name
|
||||
- data
|
||||
- orgId
|
||||
type: object
|
||||
RuletypesRuleViewData:
|
||||
properties:
|
||||
order:
|
||||
$ref: '#/components/schemas/RuletypesListOrder'
|
||||
query:
|
||||
type: string
|
||||
sort:
|
||||
$ref: '#/components/schemas/RuletypesListSort'
|
||||
states:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
version:
|
||||
type: string
|
||||
required:
|
||||
- version
|
||||
type: object
|
||||
RuletypesScheduleType:
|
||||
enum:
|
||||
- hourly
|
||||
@@ -13444,112 +13244,6 @@ paths:
|
||||
summary: Update org preference
|
||||
tags:
|
||||
- preferences
|
||||
/api/v1/public/dashboards/{id}:
|
||||
get:
|
||||
deprecated: false
|
||||
description: This endpoint returns the sanitized dashboard data for public access
|
||||
operationId: GetPublicDashboardData
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/DashboardtypesGettablePublicDashboardData'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: OK
|
||||
"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:
|
||||
- anonymous:
|
||||
- public-dashboard:read
|
||||
summary: Get public dashboard data
|
||||
tags:
|
||||
- dashboard
|
||||
/api/v1/public/dashboards/{id}/widgets/{idx}/query_range:
|
||||
get:
|
||||
deprecated: false
|
||||
description: This endpoint return query range results for a widget of public
|
||||
dashboard
|
||||
operationId: GetPublicDashboardWidgetQueryRange
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- in: path
|
||||
name: idx
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5QueryRangeResponse'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: OK
|
||||
"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:
|
||||
- anonymous:
|
||||
- public-dashboard:read
|
||||
summary: Get query range result
|
||||
tags:
|
||||
- dashboard
|
||||
/api/v1/roles:
|
||||
get:
|
||||
deprecated: false
|
||||
@@ -19801,74 +19495,6 @@ paths:
|
||||
summary: Get metric attributes
|
||||
tags:
|
||||
- metrics
|
||||
/api/v2/metrics/dashboards:
|
||||
get:
|
||||
deprecated: false
|
||||
description: This endpoint returns associated dashboards for a specified metric
|
||||
operationId: GetMetricDashboards
|
||||
parameters:
|
||||
- description: The name of the metric. May contain slashes (e.g. cloud-provider
|
||||
metrics like run.googleapis.com/request_latencies).
|
||||
in: query
|
||||
name: metricName
|
||||
required: true
|
||||
schema:
|
||||
description: The name of the metric. May contain slashes (e.g. cloud-provider
|
||||
metrics like run.googleapis.com/request_latencies).
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/MetricsexplorertypesMetricDashboardsResponse'
|
||||
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:
|
||||
- VIEWER
|
||||
- tokenizer:
|
||||
- VIEWER
|
||||
summary: Get metric dashboards
|
||||
tags:
|
||||
- metrics
|
||||
/api/v2/metrics/highlights:
|
||||
get:
|
||||
deprecated: false
|
||||
@@ -21260,237 +20886,11 @@ paths:
|
||||
summary: Get users by role id
|
||||
tags:
|
||||
- users
|
||||
/api/v2/rule_views:
|
||||
get:
|
||||
deprecated: false
|
||||
description: Returns every saved view in the calling user's org. Saved views
|
||||
are shared org-wide.
|
||||
operationId: ListRuleViews
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/RuletypesListableRuleViews'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: OK
|
||||
"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:
|
||||
- VIEWER
|
||||
- tokenizer:
|
||||
- VIEWER
|
||||
summary: List rule saved views
|
||||
tags:
|
||||
- rules
|
||||
post:
|
||||
deprecated: false
|
||||
description: Persists the calling user's rule listing state (query, states,
|
||||
sort, order) as a named, reusable view shared across the org.
|
||||
operationId: CreateRuleView
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RuletypesPostableRuleView'
|
||||
responses:
|
||||
"201":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/RuletypesRuleView'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: Created
|
||||
"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:
|
||||
- VIEWER
|
||||
- tokenizer:
|
||||
- VIEWER
|
||||
summary: Create rule saved view
|
||||
tags:
|
||||
- rules
|
||||
/api/v2/rule_views/{id}:
|
||||
delete:
|
||||
deprecated: false
|
||||
description: Removes a saved view. Saved views are shared org-wide. Deleting
|
||||
a non-existent view returns 404.
|
||||
operationId: DeleteRuleView
|
||||
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:
|
||||
- VIEWER
|
||||
- tokenizer:
|
||||
- VIEWER
|
||||
summary: Delete rule saved view
|
||||
tags:
|
||||
- rules
|
||||
put:
|
||||
deprecated: false
|
||||
description: Replaces a saved view's name and data. Saved views are shared org-wide.
|
||||
operationId: UpdateRuleView
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RuletypesPostableRuleView'
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/RuletypesRuleView'
|
||||
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:
|
||||
- VIEWER
|
||||
- tokenizer:
|
||||
- VIEWER
|
||||
summary: Update rule saved view
|
||||
tags:
|
||||
- rules
|
||||
/api/v2/rules:
|
||||
get:
|
||||
deprecated: true
|
||||
description: 'This endpoint lists all alert rules with their current evaluation
|
||||
state. Deprecated: use ListRulesV3, which supports filtering, sorting and
|
||||
pagination.'
|
||||
deprecated: false
|
||||
description: This endpoint lists all alert rules with their current evaluation
|
||||
state
|
||||
operationId: ListRules
|
||||
responses:
|
||||
"200":
|
||||
@@ -27272,93 +26672,6 @@ paths:
|
||||
summary: Get metric dashboards (v2)
|
||||
tags:
|
||||
- metrics
|
||||
/api/v3/rules:
|
||||
get:
|
||||
deprecated: false
|
||||
description: Returns a page of alert rules with their current evaluation state,
|
||||
trimmed to the fields the list page renders. Supports a filter DSL (`query`),
|
||||
a repeated `states` filter applied after the state overlay, sort (`updated_at`/`created_at`/`name`/`state`/`severity`),
|
||||
order (`asc`/`desc`), and offset-based pagination (`limit`/`offset`). In the
|
||||
filter DSL, a non-reserved key is matched as a rule label directly (`team
|
||||
= infra`); a key that collides with a reserved keyword matches either interpretation
|
||||
(negative operators exclude both), and `labels.<key>` targets only the label.
|
||||
The response also carries the org's label pairs and the reserved filter keys
|
||||
for building filter suggestions.
|
||||
operationId: ListRulesV3
|
||||
parameters:
|
||||
- in: query
|
||||
name: query
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: states
|
||||
schema:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
- in: query
|
||||
name: sort
|
||||
schema:
|
||||
$ref: '#/components/schemas/RuletypesListSort'
|
||||
- in: query
|
||||
name: order
|
||||
schema:
|
||||
$ref: '#/components/schemas/RuletypesListOrder'
|
||||
- in: query
|
||||
name: limit
|
||||
schema:
|
||||
type: integer
|
||||
- in: query
|
||||
name: offset
|
||||
schema:
|
||||
type: integer
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/RuletypesListableRules'
|
||||
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:
|
||||
- VIEWER
|
||||
- tokenizer:
|
||||
- VIEWER
|
||||
summary: List alert rules (v3)
|
||||
tags:
|
||||
- rules
|
||||
/api/v3/traces/{traceID}/flamegraph:
|
||||
post:
|
||||
deprecated: false
|
||||
|
||||
@@ -52,7 +52,7 @@ func (module *module) CreatePublic(ctx context.Context, orgID valuer.UUID, publi
|
||||
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
|
||||
}
|
||||
|
||||
dashboard, err := module.Get(ctx, orgID, publicDashboard.DashboardID)
|
||||
dashboard, err := module.GetV2(ctx, orgID, publicDashboard.DashboardID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -90,15 +90,6 @@ func (module *module) GetPublic(ctx context.Context, orgID valuer.UUID, dashboar
|
||||
return dashboardtypes.NewPublicDashboardFromStorablePublicDashboard(storablePublicDashboard), nil
|
||||
}
|
||||
|
||||
func (module *module) GetDashboardByPublicID(ctx context.Context, id valuer.UUID) (*dashboardtypes.Dashboard, error) {
|
||||
storableDashboard, err := module.store.GetDashboardByPublicID(ctx, id.StringValue())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dashboardtypes.NewDashboardFromStorableDashboard(storableDashboard), nil
|
||||
}
|
||||
|
||||
func (module *module) GetPublicDashboardSelectorsAndOrg(ctx context.Context, id valuer.UUID, orgs []*types.Organization) ([]coretypes.Selector, valuer.UUID, error) {
|
||||
orgIDs := make([]string, len(orgs))
|
||||
for idx, org := range orgs {
|
||||
@@ -116,24 +107,6 @@ func (module *module) GetPublicDashboardSelectorsAndOrg(ctx context.Context, id
|
||||
}, storableDashboard.OrgID, nil
|
||||
}
|
||||
|
||||
func (module *module) GetPublicWidgetQueryRange(ctx context.Context, id valuer.UUID, widgetIdx, startTime, endTime uint64) (*querybuildertypesv5.QueryRangeResponse, error) {
|
||||
ctx = ctxtypes.NewContextWithCommentVals(ctx, map[string]string{
|
||||
instrumentationtypes.CodeNamespace: "dashboard",
|
||||
instrumentationtypes.CodeFunctionName: "GetPublicWidgetQueryRange",
|
||||
})
|
||||
dashboard, err := module.GetDashboardByPublicID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
query, err := dashboard.GetWidgetQuery(startTime, endTime, widgetIdx, module.settings.Logger())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return module.querier.QueryRange(ctx, dashboard.OrgID, query)
|
||||
}
|
||||
|
||||
func (module *module) GetDashboardByPublicIDV2(ctx context.Context, id valuer.UUID) (*dashboardtypes.DashboardV2, error) {
|
||||
storableDashboard, err := module.store.GetDashboardByPublicID(ctx, id.StringValue())
|
||||
if err != nil {
|
||||
@@ -189,7 +162,7 @@ func (module *module) UpdatePublic(ctx context.Context, orgID valuer.UUID, publi
|
||||
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
|
||||
}
|
||||
|
||||
dashboard, err := module.Get(ctx, orgID, publicDashboard.DashboardID)
|
||||
dashboard, err := module.GetV2(ctx, orgID, publicDashboard.DashboardID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -200,34 +173,13 @@ func (module *module) UpdatePublic(ctx context.Context, orgID valuer.UUID, publi
|
||||
return module.store.UpdatePublic(ctx, dashboardtypes.NewStorablePublicDashboardFromPublicDashboard(publicDashboard))
|
||||
}
|
||||
|
||||
func (module *module) Delete(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error {
|
||||
dashboard, err := module.Get(ctx, orgID, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := dashboard.ErrIfNotDeletable(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if dashboard.Locked {
|
||||
return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "dashboard is locked, please unlock the dashboard to be delete it")
|
||||
}
|
||||
|
||||
return module.delete(ctx, orgID, id)
|
||||
}
|
||||
|
||||
func (module *module) DeleteUnsafe(ctx context.Context, orgID, id valuer.UUID) error {
|
||||
return module.delete(ctx, orgID, id)
|
||||
}
|
||||
|
||||
func (module *module) DeletePublic(ctx context.Context, orgID valuer.UUID, dashboardID valuer.UUID) error {
|
||||
_, err := module.licensing.GetActive(ctx, orgID)
|
||||
if err != nil {
|
||||
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
|
||||
}
|
||||
|
||||
dashboard, err := module.Get(ctx, orgID, dashboardID)
|
||||
dashboard, err := module.GetV2(ctx, orgID, dashboardID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -260,10 +212,6 @@ func (module *module) Collect(ctx context.Context, orgID valuer.UUID) (map[strin
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
func (module *module) Create(ctx context.Context, orgID valuer.UUID, createdBy string, creator valuer.UUID, source dashboardtypes.Source, data dashboardtypes.PostableDashboard) (*dashboardtypes.Dashboard, error) {
|
||||
return module.pkgDashboardModule.Create(ctx, orgID, createdBy, creator, source, data)
|
||||
}
|
||||
|
||||
func (module *module) CreateV2(ctx context.Context, orgID valuer.UUID, createdBy string, creator valuer.UUID, source dashboardtypes.Source, postable dashboardtypes.PostableDashboardV2) (*dashboardtypes.DashboardV2, error) {
|
||||
return module.pkgDashboardModule.CreateV2(ctx, orgID, createdBy, creator, source, postable)
|
||||
}
|
||||
@@ -346,30 +294,10 @@ func (module *module) DeleteView(ctx context.Context, orgID valuer.UUID, id valu
|
||||
return module.pkgDashboardModule.DeleteView(ctx, orgID, id)
|
||||
}
|
||||
|
||||
func (module *module) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*dashboardtypes.Dashboard, error) {
|
||||
return module.pkgDashboardModule.Get(ctx, orgID, id)
|
||||
}
|
||||
|
||||
func (module *module) GetByMetricNames(ctx context.Context, orgID valuer.UUID, metricNames []string) (map[string][]dashboardtypes.DashboardPanelRef, error) {
|
||||
return module.pkgDashboardModule.GetByMetricNames(ctx, orgID, metricNames)
|
||||
}
|
||||
|
||||
func (module *module) GetByMetricNamesV2(ctx context.Context, orgID valuer.UUID, metricNames []string) (map[string][]dashboardtypes.DashboardPanelRef, error) {
|
||||
return module.pkgDashboardModule.GetByMetricNamesV2(ctx, orgID, metricNames)
|
||||
}
|
||||
|
||||
func (module *module) List(ctx context.Context, orgID valuer.UUID) ([]*dashboardtypes.Dashboard, error) {
|
||||
return module.pkgDashboardModule.List(ctx, orgID)
|
||||
}
|
||||
|
||||
func (module *module) Update(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, data dashboardtypes.UpdatableDashboard, diff int) (*dashboardtypes.Dashboard, error) {
|
||||
return module.pkgDashboardModule.Update(ctx, orgID, id, updatedBy, data, diff)
|
||||
}
|
||||
|
||||
func (module *module) LockUnlock(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, isAdmin bool, lock bool) error {
|
||||
return module.pkgDashboardModule.LockUnlock(ctx, orgID, id, updatedBy, isAdmin, lock)
|
||||
}
|
||||
|
||||
func (module *module) ReconcileSystemDashboards(ctx context.Context, orgID valuer.UUID) error {
|
||||
return module.pkgDashboardModule.ReconcileSystemDashboards(ctx, orgID)
|
||||
}
|
||||
@@ -377,12 +305,3 @@ func (module *module) ReconcileSystemDashboards(ctx context.Context, orgID value
|
||||
func (module *module) GetSystemDashboard(ctx context.Context, orgID valuer.UUID, name string) (*dashboardtypes.DashboardV2, error) {
|
||||
return module.pkgDashboardModule.GetSystemDashboard(ctx, orgID, name)
|
||||
}
|
||||
|
||||
func (module *module) delete(ctx context.Context, orgID, id valuer.UUID) error {
|
||||
return module.store.RunInTx(ctx, func(ctx context.Context) error {
|
||||
if err := module.store.DeletePublic(ctx, id.String()); err != nil && !errors.Ast(err, errors.TypeNotFound) {
|
||||
return err
|
||||
}
|
||||
return module.store.Delete(ctx, orgID, id)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -462,7 +462,7 @@ func (m *module) relatedAssetImpact(ctx context.Context, orgID valuer.UUID, metr
|
||||
droppedSet[label] = struct{}{}
|
||||
}
|
||||
|
||||
if dashboards, err := m.dashboard.GetByMetricNames(ctx, orgID, []string{metricName}); err != nil {
|
||||
if dashboards, err := m.dashboard.GetByMetricNamesV2(ctx, orgID, []string{metricName}); err != nil {
|
||||
m.logger.WarnContext(ctx, "failed to fetch related dashboards for reduction preview", slog.String("metric_name", metricName), errors.Attr(err))
|
||||
} else {
|
||||
for _, item := range dashboards[metricName] {
|
||||
|
||||
@@ -23,15 +23,6 @@ func (f *formatter) JSONExtractString(column, path string) []byte {
|
||||
return append(f.TextToJsonColumn(column), ops...)
|
||||
}
|
||||
|
||||
func (f *formatter) JSONExtractMapValue(column, mapField, key string) []byte {
|
||||
sql := f.TextToJsonColumn(column)
|
||||
sql = append(sql, "->"...)
|
||||
sql = schema.Append(f.bunf, sql, mapField)
|
||||
sql = append(sql, "->>"...)
|
||||
sql = schema.Append(f.bunf, sql, key)
|
||||
return sql
|
||||
}
|
||||
|
||||
func (f *formatter) JSONType(column, path string) []byte {
|
||||
var sql []byte
|
||||
sql = append(sql, "jsonb_typeof("...)
|
||||
|
||||
@@ -55,67 +55,6 @@ func TestJSONExtractString(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSONExtractMapValue(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
column string
|
||||
mapField string
|
||||
key string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "plain key",
|
||||
column: "data",
|
||||
mapField: "labels",
|
||||
key: "team",
|
||||
expected: `"data"::jsonb->'labels'->>'team'`,
|
||||
},
|
||||
{
|
||||
name: "dotted key stays one map entry",
|
||||
column: "data",
|
||||
mapField: "labels",
|
||||
key: "k8s.cluster",
|
||||
expected: `"data"::jsonb->'labels'->>'k8s.cluster'`,
|
||||
},
|
||||
{
|
||||
name: "single quote in key is doubled",
|
||||
column: "data",
|
||||
mapField: "labels",
|
||||
key: "o'brien",
|
||||
expected: `"data"::jsonb->'labels'->>'o''brien'`,
|
||||
},
|
||||
{
|
||||
name: "backslash in key stays literal",
|
||||
column: "data",
|
||||
mapField: "labels",
|
||||
key: `a\b`,
|
||||
expected: `"data"::jsonb->'labels'->>'a\b'`,
|
||||
},
|
||||
{
|
||||
name: "double quote in key stays literal",
|
||||
column: "data",
|
||||
mapField: "labels",
|
||||
key: `a"b`,
|
||||
expected: `"data"::jsonb->'labels'->>'a"b'`,
|
||||
},
|
||||
{
|
||||
name: "qualified column",
|
||||
column: "rule.data",
|
||||
mapField: "labels",
|
||||
key: "severity",
|
||||
expected: `"rule"."data"::jsonb->'labels'->>'severity'`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
f := newFormatter(pgdialect.New())
|
||||
got := string(f.JSONExtractMapValue(tt.column, tt.mapField, tt.key))
|
||||
assert.Equal(t, tt.expected, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSONType(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
@@ -36,16 +36,12 @@ import type {
|
||||
GetDashboardV2200,
|
||||
GetDashboardV2PathParameters,
|
||||
GetPublicDashboard200,
|
||||
GetPublicDashboardData200,
|
||||
GetPublicDashboardDataPathParameters,
|
||||
GetPublicDashboardDataV2200,
|
||||
GetPublicDashboardDataV2PathParameters,
|
||||
GetPublicDashboardPanelQueryRangeV2200,
|
||||
GetPublicDashboardPanelQueryRangeV2Params,
|
||||
GetPublicDashboardPanelQueryRangeV2PathParameters,
|
||||
GetPublicDashboardPathParameters,
|
||||
GetPublicDashboardWidgetQueryRange200,
|
||||
GetPublicDashboardWidgetQueryRangePathParameters,
|
||||
GetSystemDashboard200,
|
||||
GetSystemDashboardPathParameters,
|
||||
ListDashboardViews200,
|
||||
@@ -474,217 +470,6 @@ export const useUpdatePublicDashboard = <
|
||||
> => {
|
||||
return useMutation(getUpdatePublicDashboardMutationOptions(options));
|
||||
};
|
||||
/**
|
||||
* This endpoint returns the sanitized dashboard data for public access
|
||||
* @summary Get public dashboard data
|
||||
*/
|
||||
export const getPublicDashboardData = (
|
||||
{ id }: GetPublicDashboardDataPathParameters,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<GetPublicDashboardData200>({
|
||||
url: `/api/v1/public/dashboards/${id}`,
|
||||
method: 'GET',
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getGetPublicDashboardDataQueryKey = ({
|
||||
id,
|
||||
}: GetPublicDashboardDataPathParameters) => {
|
||||
return [`/api/v1/public/dashboards/${id}`] as const;
|
||||
};
|
||||
|
||||
export const getGetPublicDashboardDataQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof getPublicDashboardData>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(
|
||||
{ id }: GetPublicDashboardDataPathParameters,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getPublicDashboardData>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
) => {
|
||||
const { query: queryOptions } = options ?? {};
|
||||
|
||||
const queryKey =
|
||||
queryOptions?.queryKey ?? getGetPublicDashboardDataQueryKey({ id });
|
||||
|
||||
const queryFn: QueryFunction<
|
||||
Awaited<ReturnType<typeof getPublicDashboardData>>
|
||||
> = ({ signal }) => getPublicDashboardData({ id }, signal);
|
||||
|
||||
return {
|
||||
queryKey,
|
||||
queryFn,
|
||||
enabled: id !== null && id !== undefined,
|
||||
...queryOptions,
|
||||
} as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getPublicDashboardData>>,
|
||||
TError,
|
||||
TData
|
||||
> & { queryKey: QueryKey };
|
||||
};
|
||||
|
||||
export type GetPublicDashboardDataQueryResult = NonNullable<
|
||||
Awaited<ReturnType<typeof getPublicDashboardData>>
|
||||
>;
|
||||
export type GetPublicDashboardDataQueryError =
|
||||
ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Get public dashboard data
|
||||
*/
|
||||
|
||||
export function useGetPublicDashboardData<
|
||||
TData = Awaited<ReturnType<typeof getPublicDashboardData>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(
|
||||
{ id }: GetPublicDashboardDataPathParameters,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getPublicDashboardData>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getGetPublicDashboardDataQueryOptions({ id }, options);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
};
|
||||
|
||||
return withQueryKey(query, queryOptions.queryKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get public dashboard data
|
||||
*/
|
||||
export const invalidateGetPublicDashboardData = async (
|
||||
queryClient: QueryClient,
|
||||
{ id }: GetPublicDashboardDataPathParameters,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
{ queryKey: getGetPublicDashboardDataQueryKey({ id }) },
|
||||
options,
|
||||
);
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
/**
|
||||
* This endpoint return query range results for a widget of public dashboard
|
||||
* @summary Get query range result
|
||||
*/
|
||||
export const getPublicDashboardWidgetQueryRange = (
|
||||
{ id, idx }: GetPublicDashboardWidgetQueryRangePathParameters,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<GetPublicDashboardWidgetQueryRange200>({
|
||||
url: `/api/v1/public/dashboards/${id}/widgets/${idx}/query_range`,
|
||||
method: 'GET',
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getGetPublicDashboardWidgetQueryRangeQueryKey = ({
|
||||
id,
|
||||
idx,
|
||||
}: GetPublicDashboardWidgetQueryRangePathParameters) => {
|
||||
return [`/api/v1/public/dashboards/${id}/widgets/${idx}/query_range`] as const;
|
||||
};
|
||||
|
||||
export const getGetPublicDashboardWidgetQueryRangeQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof getPublicDashboardWidgetQueryRange>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(
|
||||
{ id, idx }: GetPublicDashboardWidgetQueryRangePathParameters,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getPublicDashboardWidgetQueryRange>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
) => {
|
||||
const { query: queryOptions } = options ?? {};
|
||||
|
||||
const queryKey =
|
||||
queryOptions?.queryKey ??
|
||||
getGetPublicDashboardWidgetQueryRangeQueryKey({ id, idx });
|
||||
|
||||
const queryFn: QueryFunction<
|
||||
Awaited<ReturnType<typeof getPublicDashboardWidgetQueryRange>>
|
||||
> = ({ signal }) => getPublicDashboardWidgetQueryRange({ id, idx }, signal);
|
||||
|
||||
return {
|
||||
queryKey,
|
||||
queryFn,
|
||||
enabled: id !== null && id !== undefined && idx !== null && idx !== undefined,
|
||||
...queryOptions,
|
||||
} as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getPublicDashboardWidgetQueryRange>>,
|
||||
TError,
|
||||
TData
|
||||
> & { queryKey: QueryKey };
|
||||
};
|
||||
|
||||
export type GetPublicDashboardWidgetQueryRangeQueryResult = NonNullable<
|
||||
Awaited<ReturnType<typeof getPublicDashboardWidgetQueryRange>>
|
||||
>;
|
||||
export type GetPublicDashboardWidgetQueryRangeQueryError =
|
||||
ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Get query range result
|
||||
*/
|
||||
|
||||
export function useGetPublicDashboardWidgetQueryRange<
|
||||
TData = Awaited<ReturnType<typeof getPublicDashboardWidgetQueryRange>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(
|
||||
{ id, idx }: GetPublicDashboardWidgetQueryRangePathParameters,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getPublicDashboardWidgetQueryRange>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getGetPublicDashboardWidgetQueryRangeQueryOptions(
|
||||
{ id, idx },
|
||||
options,
|
||||
);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
};
|
||||
|
||||
return withQueryKey(query, queryOptions.queryKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get query range result
|
||||
*/
|
||||
export const invalidateGetPublicDashboardWidgetQueryRange = async (
|
||||
queryClient: QueryClient,
|
||||
{ id, idx }: GetPublicDashboardWidgetQueryRangePathParameters,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
{ queryKey: getGetPublicDashboardWidgetQueryRangeQueryKey({ id, idx }) },
|
||||
options,
|
||||
);
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns every saved view in the calling user's org. Saved views are shared org-wide.
|
||||
* @summary List dashboard saved views
|
||||
|
||||
@@ -24,8 +24,6 @@ import type {
|
||||
GetMetricAlertsParams,
|
||||
GetMetricAttributes200,
|
||||
GetMetricAttributesParams,
|
||||
GetMetricDashboards200,
|
||||
GetMetricDashboardsParams,
|
||||
GetMetricDashboardsV2200,
|
||||
GetMetricDashboardsV2Params,
|
||||
GetMetricHighlights200,
|
||||
@@ -1096,104 +1094,6 @@ export const invalidateGetMetricAttributes = async (
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
/**
|
||||
* This endpoint returns associated dashboards for a specified metric
|
||||
* @summary Get metric dashboards
|
||||
*/
|
||||
export const getMetricDashboards = (
|
||||
params: GetMetricDashboardsParams,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<GetMetricDashboards200>({
|
||||
url: `/api/v2/metrics/dashboards`,
|
||||
method: 'GET',
|
||||
params,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getGetMetricDashboardsQueryKey = (
|
||||
params?: GetMetricDashboardsParams,
|
||||
) => {
|
||||
return [`/api/v2/metrics/dashboards`, ...(params ? [params] : [])] as const;
|
||||
};
|
||||
|
||||
export const getGetMetricDashboardsQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof getMetricDashboards>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(
|
||||
params: GetMetricDashboardsParams,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getMetricDashboards>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
) => {
|
||||
const { query: queryOptions } = options ?? {};
|
||||
|
||||
const queryKey =
|
||||
queryOptions?.queryKey ?? getGetMetricDashboardsQueryKey(params);
|
||||
|
||||
const queryFn: QueryFunction<
|
||||
Awaited<ReturnType<typeof getMetricDashboards>>
|
||||
> = ({ signal }) => getMetricDashboards(params, signal);
|
||||
|
||||
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getMetricDashboards>>,
|
||||
TError,
|
||||
TData
|
||||
> & { queryKey: QueryKey };
|
||||
};
|
||||
|
||||
export type GetMetricDashboardsQueryResult = NonNullable<
|
||||
Awaited<ReturnType<typeof getMetricDashboards>>
|
||||
>;
|
||||
export type GetMetricDashboardsQueryError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Get metric dashboards
|
||||
*/
|
||||
|
||||
export function useGetMetricDashboards<
|
||||
TData = Awaited<ReturnType<typeof getMetricDashboards>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(
|
||||
params: GetMetricDashboardsParams,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getMetricDashboards>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getGetMetricDashboardsQueryOptions(params, options);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
};
|
||||
|
||||
return withQueryKey(query, queryOptions.queryKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get metric dashboards
|
||||
*/
|
||||
export const invalidateGetMetricDashboards = async (
|
||||
queryClient: QueryClient,
|
||||
params: GetMetricDashboardsParams,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
{ queryKey: getGetMetricDashboardsQueryKey(params) },
|
||||
options,
|
||||
);
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
/**
|
||||
* This endpoint returns highlights like number of datapoints, totaltimeseries, active time series, last received time for a specified metric
|
||||
* @summary Get metric highlights
|
||||
|
||||
@@ -19,9 +19,7 @@ import type {
|
||||
|
||||
import type {
|
||||
CreateRule201,
|
||||
CreateRuleView201,
|
||||
DeleteRuleByIDPathParameters,
|
||||
DeleteRuleViewPathParameters,
|
||||
GetRuleByID200,
|
||||
GetRuleByIDPathParameters,
|
||||
GetRuleHistoryFilterKeys200,
|
||||
@@ -42,19 +40,13 @@ import type {
|
||||
GetRuleHistoryTopContributors200,
|
||||
GetRuleHistoryTopContributorsParams,
|
||||
GetRuleHistoryTopContributorsPathParameters,
|
||||
ListRuleViews200,
|
||||
ListRules200,
|
||||
ListRulesV3200,
|
||||
ListRulesV3Params,
|
||||
PatchRuleByID200,
|
||||
PatchRuleByIDPathParameters,
|
||||
RenderErrorResponseDTO,
|
||||
RuletypesPostableRuleDTO,
|
||||
RuletypesPostableRuleViewDTO,
|
||||
TestRule200,
|
||||
UpdateRuleByIDPathParameters,
|
||||
UpdateRuleView200,
|
||||
UpdateRuleViewPathParameters,
|
||||
} from '../sigNoz.schemas';
|
||||
|
||||
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
|
||||
@@ -81,353 +73,7 @@ const withQueryKey = <T extends object, K>(
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns every saved view in the calling user's org. Saved views are shared org-wide.
|
||||
* @summary List rule saved views
|
||||
*/
|
||||
export const listRuleViews = (signal?: AbortSignal) => {
|
||||
return GeneratedAPIInstance<ListRuleViews200>({
|
||||
url: `/api/v2/rule_views`,
|
||||
method: 'GET',
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getListRuleViewsQueryKey = () => {
|
||||
return [`/api/v2/rule_views`] as const;
|
||||
};
|
||||
|
||||
export const getListRuleViewsQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof listRuleViews>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof listRuleViews>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
}) => {
|
||||
const { query: queryOptions } = options ?? {};
|
||||
|
||||
const queryKey = queryOptions?.queryKey ?? getListRuleViewsQueryKey();
|
||||
|
||||
const queryFn: QueryFunction<Awaited<ReturnType<typeof listRuleViews>>> = ({
|
||||
signal,
|
||||
}) => listRuleViews(signal);
|
||||
|
||||
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof listRuleViews>>,
|
||||
TError,
|
||||
TData
|
||||
> & { queryKey: QueryKey };
|
||||
};
|
||||
|
||||
export type ListRuleViewsQueryResult = NonNullable<
|
||||
Awaited<ReturnType<typeof listRuleViews>>
|
||||
>;
|
||||
export type ListRuleViewsQueryError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary List rule saved views
|
||||
*/
|
||||
|
||||
export function useListRuleViews<
|
||||
TData = Awaited<ReturnType<typeof listRuleViews>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof listRuleViews>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getListRuleViewsQueryOptions(options);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
};
|
||||
|
||||
return withQueryKey(query, queryOptions.queryKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary List rule saved views
|
||||
*/
|
||||
export const invalidateListRuleViews = async (
|
||||
queryClient: QueryClient,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
{ queryKey: getListRuleViewsQueryKey() },
|
||||
options,
|
||||
);
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
/**
|
||||
* Persists the calling user's rule listing state (query, states, sort, order) as a named, reusable view shared across the org.
|
||||
* @summary Create rule saved view
|
||||
*/
|
||||
export const createRuleView = (
|
||||
ruletypesPostableRuleViewDTO?: BodyType<RuletypesPostableRuleViewDTO>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<CreateRuleView201>({
|
||||
url: `/api/v2/rule_views`,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: ruletypesPostableRuleViewDTO,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getCreateRuleViewMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createRuleView>>,
|
||||
TError,
|
||||
{ data?: BodyType<RuletypesPostableRuleViewDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createRuleView>>,
|
||||
TError,
|
||||
{ data?: BodyType<RuletypesPostableRuleViewDTO> },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['createRuleView'];
|
||||
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 createRuleView>>,
|
||||
{ data?: BodyType<RuletypesPostableRuleViewDTO> }
|
||||
> = (props) => {
|
||||
const { data } = props ?? {};
|
||||
|
||||
return createRuleView(data);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type CreateRuleViewMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof createRuleView>>
|
||||
>;
|
||||
export type CreateRuleViewMutationBody =
|
||||
| BodyType<RuletypesPostableRuleViewDTO>
|
||||
| undefined;
|
||||
export type CreateRuleViewMutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Create rule saved view
|
||||
*/
|
||||
export const useCreateRuleView = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createRuleView>>,
|
||||
TError,
|
||||
{ data?: BodyType<RuletypesPostableRuleViewDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof createRuleView>>,
|
||||
TError,
|
||||
{ data?: BodyType<RuletypesPostableRuleViewDTO> },
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getCreateRuleViewMutationOptions(options));
|
||||
};
|
||||
/**
|
||||
* Removes a saved view. Saved views are shared org-wide. Deleting a non-existent view returns 404.
|
||||
* @summary Delete rule saved view
|
||||
*/
|
||||
export const deleteRuleView = (
|
||||
{ id }: DeleteRuleViewPathParameters,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<void>({
|
||||
url: `/api/v2/rule_views/${id}`,
|
||||
method: 'DELETE',
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getDeleteRuleViewMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof deleteRuleView>>,
|
||||
TError,
|
||||
{ pathParams: DeleteRuleViewPathParameters },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof deleteRuleView>>,
|
||||
TError,
|
||||
{ pathParams: DeleteRuleViewPathParameters },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['deleteRuleView'];
|
||||
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 deleteRuleView>>,
|
||||
{ pathParams: DeleteRuleViewPathParameters }
|
||||
> = (props) => {
|
||||
const { pathParams } = props ?? {};
|
||||
|
||||
return deleteRuleView(pathParams);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type DeleteRuleViewMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof deleteRuleView>>
|
||||
>;
|
||||
|
||||
export type DeleteRuleViewMutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Delete rule saved view
|
||||
*/
|
||||
export const useDeleteRuleView = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof deleteRuleView>>,
|
||||
TError,
|
||||
{ pathParams: DeleteRuleViewPathParameters },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof deleteRuleView>>,
|
||||
TError,
|
||||
{ pathParams: DeleteRuleViewPathParameters },
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getDeleteRuleViewMutationOptions(options));
|
||||
};
|
||||
/**
|
||||
* Replaces a saved view's name and data. Saved views are shared org-wide.
|
||||
* @summary Update rule saved view
|
||||
*/
|
||||
export const updateRuleView = (
|
||||
{ id }: UpdateRuleViewPathParameters,
|
||||
ruletypesPostableRuleViewDTO?: BodyType<RuletypesPostableRuleViewDTO>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<UpdateRuleView200>({
|
||||
url: `/api/v2/rule_views/${id}`,
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: ruletypesPostableRuleViewDTO,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getUpdateRuleViewMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateRuleView>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateRuleViewPathParameters;
|
||||
data?: BodyType<RuletypesPostableRuleViewDTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateRuleView>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateRuleViewPathParameters;
|
||||
data?: BodyType<RuletypesPostableRuleViewDTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['updateRuleView'];
|
||||
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 updateRuleView>>,
|
||||
{
|
||||
pathParams: UpdateRuleViewPathParameters;
|
||||
data?: BodyType<RuletypesPostableRuleViewDTO>;
|
||||
}
|
||||
> = (props) => {
|
||||
const { pathParams, data } = props ?? {};
|
||||
|
||||
return updateRuleView(pathParams, data);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type UpdateRuleViewMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof updateRuleView>>
|
||||
>;
|
||||
export type UpdateRuleViewMutationBody =
|
||||
| BodyType<RuletypesPostableRuleViewDTO>
|
||||
| undefined;
|
||||
export type UpdateRuleViewMutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Update rule saved view
|
||||
*/
|
||||
export const useUpdateRuleView = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateRuleView>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateRuleViewPathParameters;
|
||||
data?: BodyType<RuletypesPostableRuleViewDTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof updateRuleView>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateRuleViewPathParameters;
|
||||
data?: BodyType<RuletypesPostableRuleViewDTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getUpdateRuleViewMutationOptions(options));
|
||||
};
|
||||
/**
|
||||
* This endpoint lists all alert rules with their current evaluation state. Deprecated: use ListRulesV3, which supports filtering, sorting and pagination.
|
||||
* @deprecated
|
||||
* This endpoint lists all alert rules with their current evaluation state
|
||||
* @summary List alert rules
|
||||
*/
|
||||
export const listRules = (signal?: AbortSignal) => {
|
||||
@@ -469,7 +115,6 @@ export type ListRulesQueryResult = NonNullable<
|
||||
export type ListRulesQueryError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* @summary List alert rules
|
||||
*/
|
||||
|
||||
@@ -489,7 +134,6 @@ export function useListRules<
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* @summary List alert rules
|
||||
*/
|
||||
export const invalidateListRules = async (
|
||||
@@ -1744,97 +1388,3 @@ export const useTestRule = <
|
||||
> => {
|
||||
return useMutation(getTestRuleMutationOptions(options));
|
||||
};
|
||||
/**
|
||||
* Returns a page of alert rules with their current evaluation state, trimmed to the fields the list page renders. Supports a filter DSL (`query`), a repeated `states` filter applied after the state overlay, sort (`updated_at`/`created_at`/`name`/`state`/`severity`), order (`asc`/`desc`), and offset-based pagination (`limit`/`offset`). In the filter DSL, a non-reserved key is matched as a rule label directly (`team = infra`); a key that collides with a reserved keyword matches either interpretation (negative operators exclude both), and `labels.<key>` targets only the label. The response also carries the org's label pairs and the reserved filter keys for building filter suggestions.
|
||||
* @summary List alert rules (v3)
|
||||
*/
|
||||
export const listRulesV3 = (
|
||||
params?: ListRulesV3Params,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<ListRulesV3200>({
|
||||
url: `/api/v3/rules`,
|
||||
method: 'GET',
|
||||
params,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getListRulesV3QueryKey = (params?: ListRulesV3Params) => {
|
||||
return [`/api/v3/rules`, ...(params ? [params] : [])] as const;
|
||||
};
|
||||
|
||||
export const getListRulesV3QueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof listRulesV3>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(
|
||||
params?: ListRulesV3Params,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof listRulesV3>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
) => {
|
||||
const { query: queryOptions } = options ?? {};
|
||||
|
||||
const queryKey = queryOptions?.queryKey ?? getListRulesV3QueryKey(params);
|
||||
|
||||
const queryFn: QueryFunction<Awaited<ReturnType<typeof listRulesV3>>> = ({
|
||||
signal,
|
||||
}) => listRulesV3(params, signal);
|
||||
|
||||
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof listRulesV3>>,
|
||||
TError,
|
||||
TData
|
||||
> & { queryKey: QueryKey };
|
||||
};
|
||||
|
||||
export type ListRulesV3QueryResult = NonNullable<
|
||||
Awaited<ReturnType<typeof listRulesV3>>
|
||||
>;
|
||||
export type ListRulesV3QueryError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary List alert rules (v3)
|
||||
*/
|
||||
|
||||
export function useListRulesV3<
|
||||
TData = Awaited<ReturnType<typeof listRulesV3>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(
|
||||
params?: ListRulesV3Params,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof listRulesV3>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getListRulesV3QueryOptions(params, options);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
};
|
||||
|
||||
return withQueryKey(query, queryOptions.queryKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary List alert rules (v3)
|
||||
*/
|
||||
export const invalidateListRulesV3 = async (
|
||||
queryClient: QueryClient,
|
||||
params?: ListRulesV3Params,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
{ queryKey: getListRulesV3QueryKey(params) },
|
||||
options,
|
||||
);
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
@@ -4886,50 +4886,6 @@ export interface DashboardtypesCustomVariableSpecDTO {
|
||||
customValue: string;
|
||||
}
|
||||
|
||||
export interface DashboardtypesStorableDashboardDataDTO {
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export enum DashboardtypesSourceDTO {
|
||||
user = 'user',
|
||||
system = 'system',
|
||||
integration = 'integration',
|
||||
}
|
||||
export interface DashboardtypesDashboardDTO {
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
createdAt?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
createdBy?: string;
|
||||
data?: DashboardtypesStorableDashboardDataDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
id?: string;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
locked?: boolean;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
org_id?: string;
|
||||
source?: DashboardtypesSourceDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
updatedAt?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
updatedBy?: string;
|
||||
}
|
||||
|
||||
export interface DashboardtypesDashboardPanelRefDTO {
|
||||
/**
|
||||
* @type string
|
||||
@@ -5868,6 +5824,11 @@ export interface DashboardtypesDashboardViewDTO {
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export enum DashboardtypesSourceDTO {
|
||||
user = 'user',
|
||||
system = 'system',
|
||||
integration = 'integration',
|
||||
}
|
||||
export interface TagtypesGettableTagDTO {
|
||||
/**
|
||||
* @type string
|
||||
@@ -5945,11 +5906,6 @@ export interface DashboardtypesGettablePublicDasbhboardDTO {
|
||||
timeRangeEnabled?: boolean;
|
||||
}
|
||||
|
||||
export interface DashboardtypesGettablePublicDashboardDataDTO {
|
||||
dashboard?: DashboardtypesDashboardDTO;
|
||||
publicDashboard?: DashboardtypesGettablePublicDasbhboardDTO;
|
||||
}
|
||||
|
||||
export interface DashboardtypesGettablePublicDashboardDataV2DTO {
|
||||
dashboard?: DashboardtypesGettableDashboardV2DTO;
|
||||
publicDashboard?: DashboardtypesGettablePublicDasbhboardDTO;
|
||||
@@ -9022,25 +8978,6 @@ export interface MetricsexplorertypesMetricAttributesResponseDTO {
|
||||
totalKeys: number;
|
||||
}
|
||||
|
||||
export interface MetricsexplorertypesMetricDashboardDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
dashboardId: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
dashboardName: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
widgetId: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
widgetName: string;
|
||||
}
|
||||
|
||||
export interface MetricsexplorertypesMetricDashboardPanelsResponseDTO {
|
||||
/**
|
||||
* @type array,null
|
||||
@@ -9048,13 +8985,6 @@ export interface MetricsexplorertypesMetricDashboardPanelsResponseDTO {
|
||||
dashboards: DashboardtypesDashboardPanelRefDTO[] | null;
|
||||
}
|
||||
|
||||
export interface MetricsexplorertypesMetricDashboardsResponseDTO {
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
dashboards: MetricsexplorertypesMetricDashboardDTO[] | null;
|
||||
}
|
||||
|
||||
export interface MetricsexplorertypesMetricHighlightsResponseDTO {
|
||||
/**
|
||||
* @type integer
|
||||
@@ -10258,149 +10188,6 @@ export interface RuletypesGettableTestRuleDTO {
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface RuletypesLabelPairDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
key: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
value: string;
|
||||
}
|
||||
|
||||
export enum RuletypesListOrderDTO {
|
||||
asc = 'asc',
|
||||
desc = 'desc',
|
||||
}
|
||||
export enum RuletypesListSortDTO {
|
||||
updated_at = 'updated_at',
|
||||
created_at = 'created_at',
|
||||
name = 'name',
|
||||
state = 'state',
|
||||
severity = 'severity',
|
||||
}
|
||||
export type RuletypesListableRuleDTOLabels = { [key: string]: string };
|
||||
|
||||
export enum RuletypesRuleTypeDTO {
|
||||
threshold_rule = 'threshold_rule',
|
||||
promql_rule = 'promql_rule',
|
||||
anomaly_rule = 'anomaly_rule',
|
||||
}
|
||||
export interface RuletypesListableRuleDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
alert: string;
|
||||
alertType: RuletypesAlertTypeDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
createdAt?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
createdBy?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
description?: string;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
disabled?: boolean;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* @type object
|
||||
*/
|
||||
labels?: RuletypesListableRuleDTOLabels;
|
||||
ruleType: RuletypesRuleTypeDTO;
|
||||
state: RuletypesAlertStateDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
updatedAt?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
updatedBy?: string;
|
||||
}
|
||||
|
||||
export interface RuletypesRuleViewDataDTO {
|
||||
order?: RuletypesListOrderDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
query?: string;
|
||||
sort?: RuletypesListSortDTO;
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
states?: string[];
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
version: string;
|
||||
}
|
||||
|
||||
export interface RuletypesRuleViewDTO {
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
createdAt?: string;
|
||||
data: RuletypesRuleViewDataDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
orgId: string;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export interface RuletypesListableRuleViewsDTO {
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
views: RuletypesRuleViewDTO[];
|
||||
}
|
||||
|
||||
export interface RuletypesListableRulesDTO {
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
labels: RuletypesLabelPairDTO[];
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
reservedKeywords: string[];
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
rules: RuletypesListableRuleDTO[];
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
*/
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface RuletypesRenotifyDTO {
|
||||
/**
|
||||
* @type array,null
|
||||
@@ -10497,6 +10284,11 @@ export interface RuletypesRuleConditionDTO {
|
||||
thresholds?: RuletypesRuleThresholdDataDTO;
|
||||
}
|
||||
|
||||
export enum RuletypesRuleTypeDTO {
|
||||
threshold_rule = 'threshold_rule',
|
||||
promql_rule = 'promql_rule',
|
||||
anomaly_rule = 'anomaly_rule',
|
||||
}
|
||||
export interface RuletypesPostableRuleDTO {
|
||||
/**
|
||||
* @type string
|
||||
@@ -10549,14 +10341,6 @@ export interface RuletypesPostableRuleDTO {
|
||||
version?: string;
|
||||
}
|
||||
|
||||
export interface RuletypesPostableRuleViewDTO {
|
||||
data: RuletypesRuleViewDataDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
name: string;
|
||||
}
|
||||
|
||||
export type RuletypesRuleDTOAnnotations = { [key: string]: string };
|
||||
|
||||
export type RuletypesRuleDTOLabels = { [key: string]: string };
|
||||
@@ -12634,29 +12418,6 @@ export type GetOrgPreference200 = {
|
||||
export type UpdateOrgPreferencePathParameters = {
|
||||
name: string;
|
||||
};
|
||||
export type GetPublicDashboardDataPathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type GetPublicDashboardData200 = {
|
||||
data: DashboardtypesGettablePublicDashboardDataDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type GetPublicDashboardWidgetQueryRangePathParameters = {
|
||||
id: string;
|
||||
idx: string;
|
||||
};
|
||||
export type GetPublicDashboardWidgetQueryRange200 = {
|
||||
data: Querybuildertypesv5QueryRangeResponseDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type ListRoles200 = {
|
||||
/**
|
||||
* @type array
|
||||
@@ -13614,22 +13375,6 @@ export type GetMetricAttributes200 = {
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type GetMetricDashboardsParams = {
|
||||
/**
|
||||
* @type string
|
||||
* @description The name of the metric. May contain slashes (e.g. cloud-provider metrics like run.googleapis.com/request_latencies).
|
||||
*/
|
||||
metricName: string;
|
||||
};
|
||||
|
||||
export type GetMetricDashboards200 = {
|
||||
data: MetricsexplorertypesMetricDashboardsResponseDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type GetMetricHighlightsParams = {
|
||||
/**
|
||||
* @type string
|
||||
@@ -13883,36 +13628,6 @@ export type GetUsersByRoleID200 = {
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type ListRuleViews200 = {
|
||||
data: RuletypesListableRuleViewsDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type CreateRuleView201 = {
|
||||
data: RuletypesRuleViewDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type DeleteRuleViewPathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type UpdateRuleViewPathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type UpdateRuleView200 = {
|
||||
data: RuletypesRuleViewDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type ListRules200 = {
|
||||
/**
|
||||
* @type array
|
||||
@@ -14474,45 +14189,6 @@ export type GetMetricDashboardsV2200 = {
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type ListRulesV3Params = {
|
||||
/**
|
||||
* @type string
|
||||
* @description undefined
|
||||
*/
|
||||
query?: string;
|
||||
/**
|
||||
* @type array
|
||||
* @description undefined
|
||||
*/
|
||||
states?: string[];
|
||||
/**
|
||||
* @description undefined
|
||||
*/
|
||||
sort?: RuletypesListSortDTO;
|
||||
/**
|
||||
* @description undefined
|
||||
*/
|
||||
order?: RuletypesListOrderDTO;
|
||||
/**
|
||||
* @type integer
|
||||
* @description undefined
|
||||
*/
|
||||
limit?: number;
|
||||
/**
|
||||
* @type integer
|
||||
* @description undefined
|
||||
*/
|
||||
offset?: number;
|
||||
};
|
||||
|
||||
export type ListRulesV3200 = {
|
||||
data: RuletypesListableRulesDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type GetFlamegraphPathParameters = {
|
||||
traceID: string;
|
||||
};
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
position: relative;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
|
||||
.quick-filters-settings-container {
|
||||
flex: 0 0 0;
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
// The one `overflow: hidden` in the chain. Ancestors (RouteTab, AppLayout)
|
||||
// only hand height down; each pane below owns its own scroll.
|
||||
.layout {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
// Positioned so overlays (settings drawer) paint above the content pane
|
||||
// without changing this pane's layout width.
|
||||
.filters {
|
||||
width: 280px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
// Bounded box for the OverlayScrollbar inside it (`.overlay-scrollbar` is
|
||||
// `height: 100%`), which owns the scrolling.
|
||||
.content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { ComponentProps, ReactNode } from 'react';
|
||||
import cx from 'classnames';
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
|
||||
import QuickFilters from '../QuickFilters';
|
||||
|
||||
import styles from './QuickFiltersLayout.module.scss';
|
||||
|
||||
// Same optionality as `<QuickFilters />` in JSX (honours its defaultProps).
|
||||
type QuickFiltersElementProps = JSX.LibraryManagedAttributes<
|
||||
typeof QuickFilters,
|
||||
ComponentProps<typeof QuickFilters>
|
||||
>;
|
||||
|
||||
export interface QuickFiltersLayoutProps {
|
||||
quickFilterProps: QuickFiltersElementProps;
|
||||
showFilters: boolean;
|
||||
className?: string;
|
||||
contentClassName?: string;
|
||||
testId?: string;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
function QuickFiltersLayout({
|
||||
quickFilterProps,
|
||||
showFilters,
|
||||
className,
|
||||
contentClassName,
|
||||
testId,
|
||||
children,
|
||||
}: QuickFiltersLayoutProps): JSX.Element {
|
||||
return (
|
||||
<div className={cx(styles.layout, className)} data-testid={testId}>
|
||||
{showFilters && (
|
||||
<aside
|
||||
className={styles.filters}
|
||||
data-testid="quick-filters-layout-filters"
|
||||
>
|
||||
<QuickFilters {...quickFilterProps} />
|
||||
</aside>
|
||||
)}
|
||||
<section
|
||||
className={cx(styles.content, contentClassName)}
|
||||
data-testid="quick-filters-layout-content"
|
||||
>
|
||||
<OverlayScrollbar>
|
||||
<div>{children}</div>
|
||||
</OverlayScrollbar>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default QuickFiltersLayout;
|
||||
@@ -0,0 +1,79 @@
|
||||
import { render, screen } from 'tests/test-utils';
|
||||
|
||||
import { QuickFiltersSource } from '../../types';
|
||||
import QuickFiltersLayout from '../QuickFiltersLayout';
|
||||
|
||||
jest.mock('../QuickFiltersLayout.module.scss', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
layout: 'layout',
|
||||
filters: 'filters',
|
||||
content: 'content',
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('../../QuickFilters', () => ({
|
||||
__esModule: true,
|
||||
default: ({ source }: { source: string }): JSX.Element => (
|
||||
<div data-testid="quick-filters">{source}</div>
|
||||
),
|
||||
}));
|
||||
|
||||
const quickFilterProps = {
|
||||
source: QuickFiltersSource.TRACES_EXPLORER,
|
||||
handleFilterVisibilityChange: jest.fn(),
|
||||
};
|
||||
|
||||
describe('QuickFiltersLayout', () => {
|
||||
it('renders QuickFilters with the given props inside the filters pane', () => {
|
||||
render(
|
||||
<QuickFiltersLayout showFilters quickFilterProps={quickFilterProps}>
|
||||
<div>content</div>
|
||||
</QuickFiltersLayout>,
|
||||
);
|
||||
|
||||
const filtersPane = screen.getByTestId('quick-filters-layout-filters');
|
||||
expect(filtersPane).toContainElement(screen.getByTestId('quick-filters'));
|
||||
expect(screen.getByTestId('quick-filters')).toHaveTextContent(
|
||||
QuickFiltersSource.TRACES_EXPLORER,
|
||||
);
|
||||
expect(screen.getByTestId('quick-filters-layout-content')).toHaveTextContent(
|
||||
'content',
|
||||
);
|
||||
});
|
||||
|
||||
it('does not render the filters pane when showFilters is false', () => {
|
||||
render(
|
||||
<QuickFiltersLayout showFilters={false} quickFilterProps={quickFilterProps}>
|
||||
<div>content</div>
|
||||
</QuickFiltersLayout>,
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.queryByTestId('quick-filters-layout-filters'),
|
||||
).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('quick-filters')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('content')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('merges classNames onto the root and content panes', () => {
|
||||
render(
|
||||
<QuickFiltersLayout
|
||||
showFilters
|
||||
quickFilterProps={quickFilterProps}
|
||||
className="page-root"
|
||||
contentClassName="page-content"
|
||||
testId="page"
|
||||
>
|
||||
<div>content</div>
|
||||
</QuickFiltersLayout>,
|
||||
);
|
||||
|
||||
const root = screen.getByTestId('page');
|
||||
expect(root).toHaveClass('layout', 'page-root');
|
||||
expect(screen.getByTestId('quick-filters-layout-content')).toHaveClass(
|
||||
'content',
|
||||
'page-content',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -6,27 +6,12 @@
|
||||
left: 0;
|
||||
z-index: 999;
|
||||
width: 342px;
|
||||
height: 100%;
|
||||
background: var(--l1-background);
|
||||
transition: width 0.05s ease-in-out;
|
||||
overflow: hidden;
|
||||
color: var(--l1-foreground);
|
||||
|
||||
&.qf-logs-explorer {
|
||||
height: calc(100vh - 45px);
|
||||
}
|
||||
|
||||
&.qf-exceptions {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
&.qf-api-monitoring {
|
||||
height: calc(100vh - 45px);
|
||||
}
|
||||
|
||||
&.qf-traces-explorer {
|
||||
height: calc(100vh - 45px);
|
||||
}
|
||||
|
||||
&.hidden {
|
||||
width: 0;
|
||||
}
|
||||
|
||||
38
frontend/src/components/RouteTab/RouteTab.module.scss
Normal file
38
frontend/src/components/RouteTab/RouteTab.module.scss
Normal file
@@ -0,0 +1,38 @@
|
||||
// Hands the parent's height down to the active pane and lets the pane scroll
|
||||
// its own content, so TopNav and the tab bar stay put. Child combinators only
|
||||
// (nested Tabs must not be caught).
|
||||
.routeTab {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.routeTab > :global(.ant-tabs-content-holder) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.routeTab > :global(.ant-tabs-content-holder) > :global(.ant-tabs-content) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.routeTab
|
||||
> :global(.ant-tabs-content-holder)
|
||||
> :global(.ant-tabs-content)
|
||||
> :global(.ant-tabs-tabpane-active) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.routeTab
|
||||
> :global(.ant-tabs-content-holder)
|
||||
> :global(.ant-tabs-content)
|
||||
> :global(.ant-tabs-tabpane-active)
|
||||
> :global(.overlay-scrollbar) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
@@ -5,6 +5,11 @@ import { fireEvent, render, screen } from 'tests/test-utils';
|
||||
import RouteTab from './index';
|
||||
import { RouteTabProps } from './types';
|
||||
|
||||
jest.mock('./RouteTab.module.scss', () => ({
|
||||
__esModule: true,
|
||||
default: { routeTab: 'routeTab' },
|
||||
}));
|
||||
|
||||
function DummyComponent1(): JSX.Element {
|
||||
return <div>Dummy Component 1</div>;
|
||||
}
|
||||
@@ -74,6 +79,36 @@ describe('RouteTab component', () => {
|
||||
expect(history.location.pathname).toBe('/tab2');
|
||||
});
|
||||
|
||||
it('applies the layout class alongside a custom className', () => {
|
||||
const history = createMemoryHistory();
|
||||
const { container } = render(
|
||||
<Router history={history}>
|
||||
<RouteTab
|
||||
history={history}
|
||||
routes={testRoutes}
|
||||
activeKey="Tab1"
|
||||
className="custom-tabs"
|
||||
/>
|
||||
</Router>,
|
||||
);
|
||||
expect(container.querySelector('.ant-tabs')).toHaveClass(
|
||||
'routeTab',
|
||||
'custom-tabs',
|
||||
);
|
||||
});
|
||||
|
||||
it('renders the active tab content inside an overlay scrollbar', () => {
|
||||
const history = createMemoryHistory();
|
||||
const { container } = render(
|
||||
<Router history={history}>
|
||||
<RouteTab history={history} routes={testRoutes} activeKey="Tab1" />
|
||||
</Router>,
|
||||
);
|
||||
expect(
|
||||
container.querySelector('.ant-tabs-tabpane-active > .overlay-scrollbar'),
|
||||
).toHaveTextContent('Dummy Component 1');
|
||||
});
|
||||
|
||||
it('calls onChangeHandler on tab change', () => {
|
||||
const onChangeHandler = jest.fn();
|
||||
const history = createMemoryHistory();
|
||||
|
||||
@@ -5,20 +5,32 @@ import {
|
||||
useParams,
|
||||
} from 'react-router-dom';
|
||||
import { Tabs, TabsProps } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import HeaderRightSection from 'components/HeaderRightSection/HeaderRightSection';
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
|
||||
import { RouteTabProps } from './types';
|
||||
|
||||
import styles from './RouteTab.module.scss';
|
||||
|
||||
interface Params {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Each pane scrolls its own content inside an OverlayScrollbar, so the tab bar
|
||||
* stays put. Mounted as the page root the pane is bounded to the viewport; inside
|
||||
* a plain block wrapper the scroller is inert and the page scrolls as usual.
|
||||
* Pane content that needs a bounded box must size itself with `height: 100%`
|
||||
* (the scroller's viewport is block flow, so `flex: 1` has no effect there).
|
||||
*/
|
||||
function RouteTab({
|
||||
routes,
|
||||
activeKey,
|
||||
onChangeHandler,
|
||||
history,
|
||||
showRightSection,
|
||||
className,
|
||||
...rest
|
||||
}: RouteTabProps & TabsProps): JSX.Element {
|
||||
const params = useParams<Params>();
|
||||
@@ -50,11 +62,16 @@ function RouteTab({
|
||||
label: name,
|
||||
key,
|
||||
tabKey: route,
|
||||
children: <Component />,
|
||||
children: (
|
||||
<OverlayScrollbar>
|
||||
<Component />
|
||||
</OverlayScrollbar>
|
||||
),
|
||||
}));
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
className={cx(styles.routeTab, className)}
|
||||
onChange={onChange}
|
||||
destroyInactiveTabPane
|
||||
activeKey={currentRoute?.key || activeKey}
|
||||
|
||||
@@ -47,4 +47,5 @@ export enum LOCALSTORAGE {
|
||||
DASHBOARDS_LIST_VIEWS = 'DASHBOARDS_LIST_VIEWS',
|
||||
DASHBOARD_V2_PANEL_COLUMN_WIDTHS = 'DASHBOARD_V2_PANEL_COLUMN_WIDTHS',
|
||||
LLM_ATTRIBUTE_MAPPING_TEST_SPAN = 'LLM_ATTRIBUTE_MAPPING_TEST_SPAN',
|
||||
SAVED_VIEW_ENABLED = 'SAVED_VIEW_ENABLED',
|
||||
}
|
||||
|
||||
@@ -1,23 +1,15 @@
|
||||
.api-monitoring-page {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
.api-monitoring-explorer {
|
||||
.api-quick-filters-header {
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
border-right: 1px solid var(--l1-border);
|
||||
|
||||
.api-quick-filter-left-section {
|
||||
width: 0%;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
.api-quick-filters-header {
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
border-right: 1px solid var(--l1-border);
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
font-size: 14px;
|
||||
line-height: 18px;
|
||||
}
|
||||
font-size: 14px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.api-module-right-section {
|
||||
@@ -161,16 +153,6 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.filter-visible {
|
||||
.api-quick-filter-left-section {
|
||||
width: 260px;
|
||||
}
|
||||
|
||||
.api-module-right-section {
|
||||
width: calc(100% - 260px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.no-filtered-domains-message-container {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { useEffect } from 'react';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import cx from 'classnames';
|
||||
import QuickFilters from 'components/QuickFilters/QuickFilters';
|
||||
import QuickFiltersLayout from 'components/QuickFilters/QuickFiltersLayout/QuickFiltersLayout';
|
||||
import { useSignalFieldApis } from 'components/QuickFilters/hooks/useSignalFieldApis';
|
||||
import { QuickFiltersSource, SignalType } from 'components/QuickFilters/types';
|
||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||
@@ -20,20 +19,21 @@ function Explorer(): JSX.Element {
|
||||
|
||||
return (
|
||||
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
|
||||
<div className={cx('api-monitoring-page', 'filter-visible')}>
|
||||
<section className="api-quick-filter-left-section">
|
||||
<QuickFilters
|
||||
className="qf-api-monitoring"
|
||||
source={QuickFiltersSource.API_MONITORING}
|
||||
signal={SignalType.API_MONITORING}
|
||||
showFilterCollapse={false}
|
||||
showQueryName={false}
|
||||
handleFilterVisibilityChange={(): void => {}}
|
||||
useFieldApis={quickFilterFieldApis}
|
||||
/>
|
||||
</section>
|
||||
<QuickFiltersLayout
|
||||
className="api-monitoring-explorer"
|
||||
showFilters
|
||||
quickFilterProps={{
|
||||
className: 'qf-api-monitoring',
|
||||
source: QuickFiltersSource.API_MONITORING,
|
||||
signal: SignalType.API_MONITORING,
|
||||
showFilterCollapse: false,
|
||||
showQueryName: false,
|
||||
handleFilterVisibilityChange: (): void => {},
|
||||
useFieldApis: quickFilterFieldApis,
|
||||
}}
|
||||
>
|
||||
<DomainList />
|
||||
</div>
|
||||
</QuickFiltersLayout>
|
||||
</Sentry.ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -53,6 +53,10 @@
|
||||
z-index: 0;
|
||||
background: var(--l1-background);
|
||||
|
||||
// Column so the bottom strip sits under the scrolling content, not inside it.
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&.full-screen-content {
|
||||
width: 100%;
|
||||
}
|
||||
@@ -70,7 +74,9 @@
|
||||
|
||||
.chat-support-gateway {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
// Lifted above the bottom strip. Don't extend this pattern — new fixed-bottom
|
||||
// UI belongs in the bounded layout, not in another offset here.
|
||||
bottom: calc(20px + var(--bottom-strip-height, 0px));
|
||||
right: 20px;
|
||||
z-index: 1000;
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ import { USER_PREFERENCES } from 'constants/userPreferences';
|
||||
import AIAssistantModal from 'container/AIAssistant/AIAssistantModal';
|
||||
import AIAssistantPanel from 'container/AIAssistant/AIAssistantPanel';
|
||||
import { useAIAssistantStore } from 'container/AIAssistant/store/useAIAssistantStore';
|
||||
import BottomStrip from 'container/BottomStrip';
|
||||
import SideNav from 'container/SideNav';
|
||||
import TopNav from 'container/TopNav';
|
||||
import dayjs from 'dayjs';
|
||||
@@ -51,6 +52,7 @@ import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
import { useIsAIAssistantEnabled } from 'hooks/useIsAIAssistantEnabled';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { useSavedViewEnabled } from 'hooks/useSavedViewEnabled';
|
||||
import useTabVisibility from 'hooks/useTabFocus';
|
||||
import history from 'lib/history';
|
||||
import { isNull } from 'lodash-es';
|
||||
@@ -402,6 +404,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
}, [pathname]);
|
||||
|
||||
const isToDisplayLayout = isLoggedIn;
|
||||
const isSavedViewEnabled = useSavedViewEnabled();
|
||||
|
||||
const routeKey = useMemo(() => getRouteKey(pathname), [pathname]);
|
||||
const pageTitle = t(routeKey);
|
||||
@@ -868,6 +871,10 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
</OverlayScrollbar>
|
||||
</LayoutContent>
|
||||
</Sentry.ErrorBoundary>
|
||||
|
||||
{isSavedViewEnabled && isToDisplayLayout && !renderFullScreen && (
|
||||
<BottomStrip />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isLoggedIn && isAIAssistantEnabled && (
|
||||
|
||||
@@ -12,8 +12,12 @@ export const Layout = styled(LayoutComponent)`
|
||||
}
|
||||
`;
|
||||
|
||||
// Takes the height left in `.app-content` after the bottom strip.
|
||||
// `min-height: 0` is not needed right now, overlayscrollbars already sets
|
||||
// `overflow: auto` here. Kept so this does not break if that goes away.
|
||||
export const LayoutContent = styled(LayoutComponent.Content)`
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
&::-webkit-scrollbar {
|
||||
width: 0.1rem;
|
||||
}
|
||||
|
||||
40
frontend/src/container/BottomStrip/BottomStrip.module.scss
Normal file
40
frontend/src/container/BottomStrip/BottomStrip.module.scss
Normal file
@@ -0,0 +1,40 @@
|
||||
.strip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
|
||||
flex-shrink: 0;
|
||||
height: var(--bottom-strip-height);
|
||||
padding: 0 12px;
|
||||
|
||||
background: var(--l2-background);
|
||||
border-top: 1px solid var(--l2-border);
|
||||
|
||||
// font styles
|
||||
font-family: var(--font-family-sf-mono, monospace);
|
||||
font-size: 13px;
|
||||
font-weight: var(--font-weight-normal);
|
||||
line-height: var(--line-height-none);
|
||||
|
||||
// Above page content, below the body-portalled overlays that are meant to
|
||||
// cover the strip.
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.left,
|
||||
.right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
// Temporary placeholder for the left slot. Replaced later.
|
||||
.version {
|
||||
color: var(--l2-foreground);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { render } from 'tests/test-utils';
|
||||
|
||||
import BottomStrip, {
|
||||
BOTTOM_STRIP_HEIGHT,
|
||||
BOTTOM_STRIP_HEIGHT_VAR,
|
||||
BOTTOM_STRIP_ON_CLASS,
|
||||
} from '..';
|
||||
|
||||
describe('BottomStrip', () => {
|
||||
it('publishes the body class and height property while mounted', () => {
|
||||
const { unmount } = render(<BottomStrip />);
|
||||
|
||||
expect(document.body.classList.contains(BOTTOM_STRIP_ON_CLASS)).toBe(true);
|
||||
expect(document.body.style.getPropertyValue(BOTTOM_STRIP_HEIGHT_VAR)).toBe(
|
||||
`${BOTTOM_STRIP_HEIGHT}px`,
|
||||
);
|
||||
|
||||
unmount();
|
||||
|
||||
expect(document.body.classList.contains(BOTTOM_STRIP_ON_CLASS)).toBe(false);
|
||||
expect(document.body.style.getPropertyValue(BOTTOM_STRIP_HEIGHT_VAR)).toBe(
|
||||
'',
|
||||
);
|
||||
});
|
||||
|
||||
// The string is whatever the Go build injected, so it is rendered untouched —
|
||||
// same as SideNav. Release tags carry the "v", local builds do not.
|
||||
it.each([['v0.134.67'], ['main-64f1c2a']])(
|
||||
'renders the build version %p exactly as given',
|
||||
(version) => {
|
||||
const { getByTestId } = render(<BottomStrip />, undefined, {
|
||||
appContextOverrides: {
|
||||
versionData: { version, ee: 'Y', setupCompleted: true },
|
||||
},
|
||||
});
|
||||
|
||||
expect(getByTestId('bottom-strip-version')).toHaveTextContent(version);
|
||||
},
|
||||
);
|
||||
|
||||
it('renders the strip without a version when none is available', () => {
|
||||
const { getByTestId, queryByTestId } = render(<BottomStrip />, undefined, {
|
||||
appContextOverrides: { versionData: null },
|
||||
});
|
||||
|
||||
expect(getByTestId('bottom-strip')).toBeInTheDocument();
|
||||
expect(queryByTestId('bottom-strip-version')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
42
frontend/src/container/BottomStrip/index.tsx
Normal file
42
frontend/src/container/BottomStrip/index.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { useLayoutEffect } from 'react';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
|
||||
import styles from './BottomStrip.module.scss';
|
||||
|
||||
export const BOTTOM_STRIP_HEIGHT = 24;
|
||||
|
||||
export const BOTTOM_STRIP_ON_CLASS = 'bottom-strip-on';
|
||||
export const BOTTOM_STRIP_HEIGHT_VAR = '--bottom-strip-height';
|
||||
|
||||
function BottomStrip(): JSX.Element {
|
||||
const { versionData } = useAppContext();
|
||||
const version = versionData?.version?.trim();
|
||||
|
||||
useLayoutEffect(() => {
|
||||
document.body.classList.add(BOTTOM_STRIP_ON_CLASS);
|
||||
document.body.style.setProperty(
|
||||
BOTTOM_STRIP_HEIGHT_VAR,
|
||||
`${BOTTOM_STRIP_HEIGHT}px`,
|
||||
);
|
||||
|
||||
return (): void => {
|
||||
document.body.classList.remove(BOTTOM_STRIP_ON_CLASS);
|
||||
document.body.style.removeProperty(BOTTOM_STRIP_HEIGHT_VAR);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={styles.strip} data-testid="bottom-strip">
|
||||
<div className={styles.left}>
|
||||
{version && (
|
||||
<span className={styles.version} data-testid="bottom-strip-version">
|
||||
{version}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.right} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default BottomStrip;
|
||||
@@ -1,6 +1,8 @@
|
||||
.create-alert-v2-footer {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
// Lifted above the bottom strip. Don't extend this pattern — new fixed-bottom
|
||||
// UI belongs in the bounded layout, not in another offset here.
|
||||
bottom: var(--bottom-strip-height, 0px);
|
||||
left: 63px;
|
||||
right: 0;
|
||||
background-color: var(--l1-background);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
.explorer-options-container {
|
||||
position: fixed;
|
||||
bottom: 0px;
|
||||
// Lifted above the bottom strip. Don't extend this pattern — new fixed-bottom
|
||||
// UI belongs in the bounded layout, not in another offset here.
|
||||
bottom: var(--bottom-strip-height, 0px);
|
||||
left: calc(50% + 240px);
|
||||
transform: translate(calc(-50% - 120px), 0);
|
||||
transition: left 0.2s linear;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
.explorer-option-droppable-container {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
// Lifted above the bottom strip. Don't extend this pattern — new fixed-bottom
|
||||
// UI belongs in the bounded layout, not in another offset here.
|
||||
bottom: var(--bottom-strip-height, 0px);
|
||||
width: -webkit-fill-available;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
|
||||
@@ -65,8 +65,6 @@
|
||||
}
|
||||
|
||||
.trace-explorer-page {
|
||||
display: flex;
|
||||
|
||||
// Meant to fix the query builder colors
|
||||
--input-background: var(--l2-background);
|
||||
--input-hover-background: var(--l2-background);
|
||||
@@ -75,32 +73,8 @@
|
||||
--input-hover-border-color: var(--internal-ant-border-color-hover);
|
||||
--input-focus-border-color: var(--internal-ant-border-color-hover);
|
||||
|
||||
.filter {
|
||||
width: 260px;
|
||||
height: 100%;
|
||||
min-height: 100vh;
|
||||
|
||||
border-right: 0px;
|
||||
border: 1px solid var(--l1-border);
|
||||
background-color: var(--l1-background);
|
||||
|
||||
> .ant-card-body {
|
||||
padding: 0;
|
||||
width: 258px;
|
||||
}
|
||||
}
|
||||
|
||||
.trace-explorer {
|
||||
width: 100%;
|
||||
background: var(--l1-background);
|
||||
|
||||
> .ant-card-body {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
border-color: var(--l1-border);
|
||||
}
|
||||
.trace-explorer.filters-expanded {
|
||||
width: calc(100% - 260px);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,10 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { Card } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import cx from 'classnames';
|
||||
import ExplorerCard from 'components/ExplorerCard/ExplorerCard';
|
||||
import QueryCancelledPlaceholder from 'components/QueryCancelledPlaceholder';
|
||||
import QuickFilters from 'components/QuickFilters/QuickFilters';
|
||||
import QuickFiltersLayout from 'components/QuickFilters/QuickFiltersLayout/QuickFiltersLayout';
|
||||
import { useSignalFieldApis } from 'components/QuickFilters/hooks/useSignalFieldApis';
|
||||
import { QuickFiltersSource, SignalType } from 'components/QuickFilters/types';
|
||||
import WarningPopover from 'components/WarningPopover/WarningPopover';
|
||||
@@ -188,26 +186,21 @@ function Explorer(): JSX.Element {
|
||||
|
||||
return (
|
||||
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
|
||||
<div
|
||||
<QuickFiltersLayout
|
||||
className="trace-explorer-page"
|
||||
data-testid="llm-observability-explorer"
|
||||
testId="llm-observability-explorer"
|
||||
showFilters={isOpen}
|
||||
quickFilterProps={{
|
||||
className: 'qf-traces-explorer',
|
||||
source: QuickFiltersSource.AI_OBSERVABILITY,
|
||||
signal: SignalType.AI_OBSERVABILITY,
|
||||
useFieldApis: quickFiltersFieldApis,
|
||||
handleFilterVisibilityChange: (): void => {
|
||||
setOpen(!isOpen);
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Card className="filter" hidden={!isOpen}>
|
||||
<QuickFilters
|
||||
className="qf-traces-explorer"
|
||||
source={QuickFiltersSource.AI_OBSERVABILITY}
|
||||
signal={SignalType.AI_OBSERVABILITY}
|
||||
useFieldApis={quickFiltersFieldApis}
|
||||
handleFilterVisibilityChange={(): void => {
|
||||
setOpen(!isOpen);
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
<div
|
||||
className={cx('trace-explorer', {
|
||||
'filters-expanded': isOpen,
|
||||
})}
|
||||
>
|
||||
<div className="trace-explorer">
|
||||
<div className="trace-explorer-header">
|
||||
<Toolbar
|
||||
showAutoRefresh
|
||||
@@ -291,7 +284,7 @@ function Explorer(): JSX.Element {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</QuickFiltersLayout>
|
||||
</Sentry.ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,18 +1,7 @@
|
||||
.meter-explorer-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
.meter-explorer-quick-filters-section {
|
||||
width: 280px;
|
||||
border-right: 1px solid var(--l1-border);
|
||||
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.meter-explorer-content-section {
|
||||
width: 100%;
|
||||
// Clearance for the fixed ExplorerOptions bar.
|
||||
padding-bottom: 80px;
|
||||
|
||||
// Meant to fix the query builder colors
|
||||
--input-background: var(--l2-background);
|
||||
@@ -83,14 +72,6 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.quick-filters-open {
|
||||
.meter-explorer-content-section {
|
||||
width: calc(100% - 280px);
|
||||
}
|
||||
}
|
||||
|
||||
padding-bottom: 80px;
|
||||
}
|
||||
|
||||
.dashboards-and-alerts-popover-container {
|
||||
|
||||
@@ -3,9 +3,8 @@ import { useQueryClient } from 'react-query';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { Button, Tooltip } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import cx from 'classnames';
|
||||
import { QueryBuilderV2 } from 'components/QueryBuilderV2/QueryBuilderV2';
|
||||
import QuickFilters from 'components/QuickFilters/QuickFilters';
|
||||
import QuickFiltersLayout from 'components/QuickFilters/QuickFiltersLayout/QuickFiltersLayout';
|
||||
import { useSignalFieldApis } from 'components/QuickFilters/hooks/useSignalFieldApis';
|
||||
import { QuickFiltersSource, SignalType } from 'components/QuickFilters/types';
|
||||
import { initialQueryMeterWithType, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
@@ -121,29 +120,21 @@ function Explorer(): JSX.Element {
|
||||
|
||||
return (
|
||||
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
|
||||
<div
|
||||
className={cx('meter-explorer-container', {
|
||||
'quick-filters-open': showQuickFilters,
|
||||
})}
|
||||
<QuickFiltersLayout
|
||||
className="meter-explorer-container"
|
||||
showFilters={showQuickFilters}
|
||||
quickFilterProps={{
|
||||
className: 'qf-meter-explorer',
|
||||
source: QuickFiltersSource.METER_EXPLORER,
|
||||
signal: SignalType.METER_EXPLORER,
|
||||
showFilterCollapse: true,
|
||||
showQueryName: false,
|
||||
handleFilterVisibilityChange: (): void => {
|
||||
setShowQuickFilters(!showQuickFilters);
|
||||
},
|
||||
useFieldApis: quickFilterFieldApis,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={cx('meter-explorer-quick-filters-section', {
|
||||
hidden: !showQuickFilters,
|
||||
})}
|
||||
>
|
||||
<QuickFilters
|
||||
className="qf-meter-explorer"
|
||||
source={QuickFiltersSource.METER_EXPLORER}
|
||||
signal={SignalType.METER_EXPLORER}
|
||||
showFilterCollapse
|
||||
showQueryName={false}
|
||||
handleFilterVisibilityChange={(): void => {
|
||||
setShowQuickFilters(!showQuickFilters);
|
||||
}}
|
||||
useFieldApis={quickFilterFieldApis}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="meter-explorer-content-section">
|
||||
<div className="meter-explorer-explore-content">
|
||||
<div className="explore-header">
|
||||
@@ -196,7 +187,7 @@ function Explorer(): JSX.Element {
|
||||
splitedQueries={splitedQueries}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</QuickFiltersLayout>
|
||||
</Sentry.ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import {
|
||||
MetricsexplorertypesMetricAlertDTO,
|
||||
MetricsexplorertypesMetricAttributeDTO,
|
||||
MetricsexplorertypesMetricDashboardDTO,
|
||||
MetricsexplorertypesMetricHighlightsResponseDTO,
|
||||
MetricsexplorertypesMetricMetadataDTO,
|
||||
MetrictypesTemporalityDTO,
|
||||
@@ -55,8 +54,6 @@ export type MetricHighlight = MetricsexplorertypesMetricHighlightsResponseDTO;
|
||||
|
||||
export type MetricAlert = MetricsexplorertypesMetricAlertDTO;
|
||||
|
||||
export type MetricDashboard = MetricsexplorertypesMetricDashboardDTO;
|
||||
|
||||
export type MetricMetadata = MetricsexplorertypesMetricMetadataDTO;
|
||||
export interface MetricMetadataFormState {
|
||||
type: MetrictypesTypeDTO;
|
||||
|
||||
@@ -60,9 +60,6 @@
|
||||
.metrics-table-container {
|
||||
padding-bottom: 48px;
|
||||
.ant-table {
|
||||
margin-left: -16px;
|
||||
margin-right: -16px;
|
||||
|
||||
.ant-table-thead > tr > th {
|
||||
padding: 12px;
|
||||
font-weight: 500;
|
||||
@@ -184,7 +181,9 @@
|
||||
|
||||
.ant-pagination {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
// Lifted above the bottom strip. Don't extend this pattern — new
|
||||
// fixed-bottom UI belongs in the bounded layout, not in another offset here.
|
||||
bottom: var(--bottom-strip-height, 0px);
|
||||
width: calc(100% - 54px);
|
||||
background: var(--l1-background);
|
||||
padding: 16px;
|
||||
|
||||
11
frontend/src/hooks/useSavedViewEnabled.ts
Normal file
11
frontend/src/hooks/useSavedViewEnabled.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import getLocalStorageKey from 'api/browser/localstorage/get';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { useState } from 'react';
|
||||
|
||||
export function useSavedViewEnabled(): boolean {
|
||||
const [isEnabled] = useState(
|
||||
() => getLocalStorageKey(LOCALSTORAGE.SAVED_VIEW_ENABLED) === 'true',
|
||||
);
|
||||
|
||||
return isEnabled;
|
||||
}
|
||||
@@ -1,11 +1,4 @@
|
||||
.all-errors-page {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
.all-errors-quick-filter-section {
|
||||
width: 0%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.all-errors-right-section {
|
||||
.right-toolbar-actions-container {
|
||||
display: flex;
|
||||
@@ -18,14 +11,4 @@
|
||||
.ant-tabs {
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
&.filter-visible {
|
||||
.all-errors-quick-filter-section {
|
||||
width: 260px;
|
||||
}
|
||||
|
||||
.all-errors-right-section {
|
||||
width: calc(100% - 260px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,13 +5,11 @@ import { Filter } from '@signozhq/icons';
|
||||
import { Button, Tooltip } from 'antd';
|
||||
import getLocalStorageKey from 'api/browser/localstorage/get';
|
||||
import setLocalStorageApi from 'api/browser/localstorage/set';
|
||||
import cx from 'classnames';
|
||||
import HeaderRightSection from 'components/HeaderRightSection/HeaderRightSection';
|
||||
import QuickFilters from 'components/QuickFilters/QuickFilters';
|
||||
import QuickFiltersLayout from 'components/QuickFilters/QuickFiltersLayout/QuickFiltersLayout';
|
||||
import { useSignalFieldApis } from 'components/QuickFilters/hooks/useSignalFieldApis';
|
||||
import { QuickFiltersSource, SignalType } from 'components/QuickFilters/types';
|
||||
import RouteTab from 'components/RouteTab';
|
||||
import TypicalOverlayScrollbar from 'components/TypicalOverlayScrollbar/TypicalOverlayScrollbar';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import RightToolbarActions from 'container/QueryBuilder/components/ToolbarActions/RightToolbarActions';
|
||||
import ResourceAttributesFilterV2 from 'container/ResourceAttributeFilterV2/ResourceAttributesFilterV2';
|
||||
@@ -59,63 +57,52 @@ function AllErrors(): JSX.Element {
|
||||
const quickFilterFieldApis = useSignalFieldApis();
|
||||
|
||||
return (
|
||||
<div className={cx('all-errors-page', showFilters ? 'filter-visible' : '')}>
|
||||
{showFilters && (
|
||||
<section className={cx('all-errors-quick-filter-section')}>
|
||||
<QuickFilters
|
||||
className="qf-exceptions"
|
||||
source={QuickFiltersSource.EXCEPTIONS}
|
||||
signal={SignalType.EXCEPTIONS}
|
||||
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
useFieldApis={quickFilterFieldApis}
|
||||
/>
|
||||
</section>
|
||||
)}
|
||||
<section
|
||||
className={cx(
|
||||
'all-errors-right-section',
|
||||
showFilters ? 'filter-visible' : '',
|
||||
)}
|
||||
>
|
||||
<TypicalOverlayScrollbar>
|
||||
<>
|
||||
<Toolbar
|
||||
showAutoRefresh={false}
|
||||
leftActions={
|
||||
!showFilters ? (
|
||||
<Tooltip title="Show Filters">
|
||||
<Button onClick={handleFilterVisibilityChange} className="filter-btn">
|
||||
<Filter size="md" />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
) : undefined
|
||||
}
|
||||
rightActions={
|
||||
<div className="right-toolbar-actions-container">
|
||||
<RightToolbarActions
|
||||
onStageRunQuery={handleRunQuery}
|
||||
isLoadingQueries={isLoadingQueries}
|
||||
handleCancelQuery={handleCancelQuery}
|
||||
/>
|
||||
<HeaderRightSection
|
||||
enableAnnouncements={false}
|
||||
enableShare
|
||||
enableFeedback
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
<QuickFiltersLayout
|
||||
className="all-errors-page"
|
||||
contentClassName="all-errors-right-section"
|
||||
showFilters={showFilters}
|
||||
quickFilterProps={{
|
||||
className: 'qf-exceptions',
|
||||
source: QuickFiltersSource.EXCEPTIONS,
|
||||
signal: SignalType.EXCEPTIONS,
|
||||
handleFilterVisibilityChange,
|
||||
useFieldApis: quickFilterFieldApis,
|
||||
}}
|
||||
>
|
||||
<Toolbar
|
||||
showAutoRefresh={false}
|
||||
leftActions={
|
||||
!showFilters ? (
|
||||
<Tooltip title="Show Filters">
|
||||
<Button onClick={handleFilterVisibilityChange} className="filter-btn">
|
||||
<Filter size="md" />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
) : undefined
|
||||
}
|
||||
rightActions={
|
||||
<div className="right-toolbar-actions-container">
|
||||
<RightToolbarActions
|
||||
onStageRunQuery={handleRunQuery}
|
||||
isLoadingQueries={isLoadingQueries}
|
||||
handleCancelQuery={handleCancelQuery}
|
||||
/>
|
||||
<ResourceAttributesFilterV2 />
|
||||
<RouteTab
|
||||
routes={routes}
|
||||
activeKey={pathname}
|
||||
history={history}
|
||||
showRightSection={false}
|
||||
<HeaderRightSection
|
||||
enableAnnouncements={false}
|
||||
enableShare
|
||||
enableFeedback
|
||||
/>
|
||||
</>
|
||||
</TypicalOverlayScrollbar>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<ResourceAttributesFilterV2 />
|
||||
<RouteTab
|
||||
routes={routes}
|
||||
activeKey={pathname}
|
||||
history={history}
|
||||
showRightSection={false}
|
||||
/>
|
||||
</QuickFiltersLayout>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,4 @@
|
||||
.api-monitoring-page {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
|
||||
.ant-tabs {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.ant-tabs-nav {
|
||||
padding: 0 16px;
|
||||
margin-bottom: 0px;
|
||||
@@ -15,22 +8,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.ant-tabs-content-holder {
|
||||
display: flex;
|
||||
|
||||
.ant-tabs-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.ant-tabs-tabpane {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
@@ -13,9 +13,12 @@ function ApiMonitoringPage(): JSX.Element {
|
||||
const routes: TabRoutes[] = [Explorer];
|
||||
|
||||
return (
|
||||
<div className="api-monitoring-page">
|
||||
<RouteTab routes={routes} activeKey={pathname} history={history} />
|
||||
</div>
|
||||
<RouteTab
|
||||
className="api-monitoring-page"
|
||||
routes={routes}
|
||||
activeKey={pathname}
|
||||
history={history}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
// Lifted above the bottom strip. Don't extend this pattern — new fixed-bottom
|
||||
// UI belongs in the bounded layout, not in another offset here.
|
||||
bottom: var(--bottom-strip-height, 0px);
|
||||
left: 0;
|
||||
width: 100%;
|
||||
z-index: 100;
|
||||
|
||||
@@ -1,13 +1,4 @@
|
||||
.infra-monitoring-module-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
||||
.ant-tabs {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.ant-tabs-nav {
|
||||
padding: 0 8px;
|
||||
margin-bottom: 0px;
|
||||
@@ -17,22 +8,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.ant-tabs-content-holder {
|
||||
display: flex;
|
||||
|
||||
.ant-tabs-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.ant-tabs-tabpane {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
@@ -13,8 +13,11 @@ export default function InfrastructureMonitoringPage(): JSX.Element {
|
||||
const routes: TabRoutes[] = [Hosts, Kubernetes];
|
||||
|
||||
return (
|
||||
<div className="infra-monitoring-module-container">
|
||||
<RouteTab routes={routes} activeKey={pathname} history={history} />
|
||||
</div>
|
||||
<RouteTab
|
||||
className="infra-monitoring-module-container"
|
||||
routes={routes}
|
||||
activeKey={pathname}
|
||||
history={history}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,16 +1,4 @@
|
||||
.logs-module-container {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.ant-tabs {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.ant-tabs-nav {
|
||||
padding: 0 16px;
|
||||
margin-bottom: 0px;
|
||||
@@ -20,25 +8,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.ant-tabs-content-holder {
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
|
||||
.ant-tabs-content {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.ant-tabs-tabpane {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
@@ -13,8 +13,11 @@ export default function LogsModulePage(): JSX.Element {
|
||||
const routes: TabRoutes[] = [logsExplorer, logsPipelines, logSaveView];
|
||||
|
||||
return (
|
||||
<div className="logs-module-container">
|
||||
<RouteTab routes={routes} activeKey={pathname} history={history} />
|
||||
</div>
|
||||
<RouteTab
|
||||
className="logs-module-container"
|
||||
routes={routes}
|
||||
activeKey={pathname}
|
||||
history={history}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,4 @@
|
||||
.messaging-queues-module-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
||||
.ant-tabs {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.ant-tabs-nav {
|
||||
padding: 0 8px;
|
||||
margin-bottom: 0px;
|
||||
@@ -17,22 +8,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.ant-tabs-content-holder {
|
||||
display: flex;
|
||||
|
||||
.ant-tabs-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.ant-tabs-tabpane {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
@@ -68,8 +68,11 @@ export default function MessagingQueuesMainPage(): JSX.Element {
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="messaging-queues-module-container">
|
||||
<RouteTab routes={routes} activeKey={pathname} history={history} />
|
||||
</div>
|
||||
<RouteTab
|
||||
className="messaging-queues-module-container"
|
||||
routes={routes}
|
||||
activeKey={pathname}
|
||||
history={history}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,14 +14,13 @@ function MeterExplorerPage(): JSX.Element {
|
||||
const routes: TabRoutes[] = [Meter, Explorer, Views];
|
||||
|
||||
return (
|
||||
<div className="meter-explorer-page">
|
||||
<RouteTab
|
||||
routes={routes}
|
||||
activeKey={pathname}
|
||||
history={history}
|
||||
defaultActiveKey={ROUTES.METER}
|
||||
/>
|
||||
</div>
|
||||
<RouteTab
|
||||
className="meter-explorer-page"
|
||||
routes={routes}
|
||||
activeKey={pathname}
|
||||
history={history}
|
||||
defaultActiveKey={ROUTES.METER}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,4 @@
|
||||
.metrics-explorer-page {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
||||
.ant-tabs {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.ant-tabs-nav {
|
||||
padding-left: 16px;
|
||||
margin-bottom: 0px;
|
||||
@@ -18,20 +9,7 @@
|
||||
}
|
||||
|
||||
.ant-tabs-content-holder {
|
||||
display: flex;
|
||||
padding: 16px;
|
||||
|
||||
.ant-tabs-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.ant-tabs-tabpane {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
|
||||
@@ -42,9 +42,12 @@ function MetricsExplorerPage(): JSX.Element {
|
||||
useShareBuilderUrl({ defaultValue: defaultQuery });
|
||||
|
||||
return (
|
||||
<div className="metrics-explorer-page">
|
||||
<RouteTab routes={routes} activeKey={pathname} history={history} />
|
||||
</div>
|
||||
<RouteTab
|
||||
className="metrics-explorer-page"
|
||||
routes={routes}
|
||||
activeKey={pathname}
|
||||
history={history}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -65,8 +65,6 @@
|
||||
}
|
||||
|
||||
.trace-explorer-page {
|
||||
display: flex;
|
||||
|
||||
// Meant to fix the query builder colors
|
||||
--input-background: var(--l2-background);
|
||||
--input-hover-background: var(--l2-background);
|
||||
@@ -75,32 +73,8 @@
|
||||
--input-hover-border-color: var(--internal-ant-border-color-hover);
|
||||
--input-focus-border-color: var(--internal-ant-border-color-hover);
|
||||
|
||||
.filter {
|
||||
width: 260px;
|
||||
height: 100%;
|
||||
min-height: 100vh;
|
||||
|
||||
border-right: 0px;
|
||||
border: 1px solid var(--l1-border);
|
||||
background-color: var(--l1-background);
|
||||
|
||||
> .ant-card-body {
|
||||
padding: 0;
|
||||
width: 258px;
|
||||
}
|
||||
}
|
||||
|
||||
.trace-explorer {
|
||||
width: 100%;
|
||||
background: var(--l1-background);
|
||||
|
||||
> .ant-card-body {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
border-color: var(--l1-border);
|
||||
}
|
||||
.trace-explorer.filters-expanded {
|
||||
width: calc(100% - 260px);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,10 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { Card } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import cx from 'classnames';
|
||||
import ExplorerCard from 'components/ExplorerCard/ExplorerCard';
|
||||
import QueryCancelledPlaceholder from 'components/QueryCancelledPlaceholder';
|
||||
import QuickFilters from 'components/QuickFilters/QuickFilters';
|
||||
import QuickFiltersLayout from 'components/QuickFilters/QuickFiltersLayout/QuickFiltersLayout';
|
||||
import { useSignalFieldApis } from 'components/QuickFilters/hooks/useSignalFieldApis';
|
||||
import { QuickFiltersSource, SignalType } from 'components/QuickFilters/types';
|
||||
import WarningPopover from 'components/WarningPopover/WarningPopover';
|
||||
@@ -261,23 +259,20 @@ function TracesExplorer(): JSX.Element {
|
||||
|
||||
return (
|
||||
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
|
||||
<div className="trace-explorer-page">
|
||||
<Card className="filter" hidden={!isOpen}>
|
||||
<QuickFilters
|
||||
className="qf-traces-explorer"
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
signal={SignalType.TRACES}
|
||||
handleFilterVisibilityChange={(): void => {
|
||||
setOpen(!isOpen);
|
||||
}}
|
||||
useFieldApis={quickFilterFieldApis}
|
||||
/>
|
||||
</Card>
|
||||
<div
|
||||
className={cx('trace-explorer', {
|
||||
'filters-expanded': isOpen,
|
||||
})}
|
||||
>
|
||||
<QuickFiltersLayout
|
||||
className="trace-explorer-page"
|
||||
showFilters={isOpen}
|
||||
quickFilterProps={{
|
||||
className: 'qf-traces-explorer',
|
||||
source: QuickFiltersSource.TRACES_EXPLORER,
|
||||
signal: SignalType.TRACES,
|
||||
handleFilterVisibilityChange: (): void => {
|
||||
setOpen(!isOpen);
|
||||
},
|
||||
useFieldApis: quickFilterFieldApis,
|
||||
}}
|
||||
>
|
||||
<div className="trace-explorer">
|
||||
<div className="trace-explorer-header">
|
||||
<Toolbar
|
||||
showAutoRefresh
|
||||
@@ -369,7 +364,7 @@ function TracesExplorer(): JSX.Element {
|
||||
handleChangeSelectedView={handleChangeSelectedView}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</QuickFiltersLayout>
|
||||
</Sentry.ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -25,16 +25,15 @@ function TracesModulePage(): JSX.Element {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="traces-module-container">
|
||||
<RouteTab
|
||||
routes={routes}
|
||||
activeKey={
|
||||
pathname.includes(ROUTES.TRACES_FUNNELS) ? ROUTES.TRACES_FUNNELS : pathname
|
||||
}
|
||||
history={history}
|
||||
onChangeHandler={handleTabChange}
|
||||
/>
|
||||
</div>
|
||||
<RouteTab
|
||||
className="traces-module-container"
|
||||
routes={routes}
|
||||
activeKey={
|
||||
pathname.includes(ROUTES.TRACES_FUNNELS) ? ROUTES.TRACES_FUNNELS : pathname
|
||||
}
|
||||
history={history}
|
||||
onChangeHandler={handleTabChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -630,62 +630,6 @@ func (provider *provider) addDashboardRoutes(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/public/dashboards/{id}", handler.New(provider.authzMiddleware.CheckWithoutClaims(
|
||||
provider.dashboardHandler.GetPublicData,
|
||||
authtypes.Relation{Verb: coretypes.VerbRead},
|
||||
coretypes.ResourceMetaResourcePublicDashboard,
|
||||
func(req *http.Request, orgs []*types.Organization) ([]coretypes.Selector, valuer.UUID, error) {
|
||||
id, err := valuer.NewUUID(mux.Vars(req)["id"])
|
||||
if err != nil {
|
||||
return nil, valuer.UUID{}, err
|
||||
}
|
||||
|
||||
return provider.dashboardModule.GetPublicDashboardSelectorsAndOrg(req.Context(), id, orgs)
|
||||
}, []string{}), handler.OpenAPIDef{
|
||||
ID: "GetPublicDashboardData",
|
||||
Tags: []string{"dashboard"},
|
||||
Summary: "Get public dashboard data",
|
||||
Description: "This endpoint returns the sanitized dashboard data for public access",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: new(dashboardtypes.GettablePublicDashboardData),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newAnonymousSecuritySchemes([]string{coretypes.ResourceMetaResourcePublicDashboard.Scope(coretypes.VerbRead)}),
|
||||
})).Methods(http.MethodGet).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/public/dashboards/{id}/widgets/{idx}/query_range", handler.New(provider.authzMiddleware.CheckWithoutClaims(
|
||||
provider.dashboardHandler.GetPublicWidgetQueryRange,
|
||||
authtypes.Relation{Verb: coretypes.VerbRead},
|
||||
coretypes.ResourceMetaResourcePublicDashboard,
|
||||
func(req *http.Request, orgs []*types.Organization) ([]coretypes.Selector, valuer.UUID, error) {
|
||||
id, err := valuer.NewUUID(mux.Vars(req)["id"])
|
||||
if err != nil {
|
||||
return nil, valuer.UUID{}, err
|
||||
}
|
||||
|
||||
return provider.dashboardModule.GetPublicDashboardSelectorsAndOrg(req.Context(), id, orgs)
|
||||
}, []string{}), handler.OpenAPIDef{
|
||||
ID: "GetPublicDashboardWidgetQueryRange",
|
||||
Tags: []string{"dashboard"},
|
||||
Summary: "Get query range result",
|
||||
Description: "This endpoint return query range results for a widget of public dashboard",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: new(querybuildertypesv5.QueryRangeResponse),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newAnonymousSecuritySchemes([]string{coretypes.ResourceMetaResourcePublicDashboard.Scope(coretypes.VerbRead)}),
|
||||
})).Methods(http.MethodGet).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/public/dashboards/{id}", handler.New(provider.authzMiddleware.CheckWithoutClaims(
|
||||
provider.dashboardHandler.GetPublicDataV2,
|
||||
authtypes.Relation{Verb: coretypes.VerbRead},
|
||||
|
||||
@@ -167,26 +167,6 @@ func (provider *provider) addMetricsExplorerRoutes(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/metrics/dashboards", handler.New(
|
||||
provider.authzMiddleware.ViewAccess(provider.metricsExplorerHandler.GetMetricDashboards),
|
||||
handler.OpenAPIDef{
|
||||
ID: "GetMetricDashboards",
|
||||
Tags: []string{"metrics"},
|
||||
Summary: "Get metric dashboards",
|
||||
Description: "This endpoint returns associated dashboards for a specified metric",
|
||||
Request: nil,
|
||||
RequestQuery: new(metricsexplorertypes.MetricNameQuery),
|
||||
RequestContentType: "",
|
||||
Response: new(metricsexplorertypes.MetricDashboardsResponse),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusNotFound, http.StatusInternalServerError},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
})).Methods(http.MethodGet).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v3/metrics/dashboards", handler.New(
|
||||
provider.authzMiddleware.ViewAccess(provider.metricsExplorerHandler.GetMetricDashboardsV2),
|
||||
handler.OpenAPIDef{
|
||||
|
||||
@@ -15,26 +15,10 @@ func (provider *provider) addRulerRoutes(router *mux.Router) error {
|
||||
ID: "ListRules",
|
||||
Tags: []string{"rules"},
|
||||
Summary: "List alert rules",
|
||||
Description: "This endpoint lists all alert rules with their current evaluation state. Deprecated: use ListRulesV3, which supports filtering, sorting and pagination.",
|
||||
Description: "This endpoint lists all alert rules with their current evaluation state",
|
||||
Response: make([]*ruletypes.Rule, 0),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
Deprecated: true,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
})).Methods(http.MethodGet).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v3/rules", handler.New(provider.authzMiddleware.ViewAccess(provider.rulerHandler.ListRulesV3), handler.OpenAPIDef{
|
||||
ID: "ListRulesV3",
|
||||
Tags: []string{"rules"},
|
||||
Summary: "List alert rules (v3)",
|
||||
Description: "Returns a page of alert rules with their current evaluation state, trimmed to the fields the list page renders. Supports a filter DSL (`query`), a repeated `states` filter applied after the state overlay, sort (`updated_at`/`created_at`/`name`/`state`/`severity`), order (`asc`/`desc`), and offset-based pagination (`limit`/`offset`). In the filter DSL, a non-reserved key is matched as a rule label directly (`team = infra`); a key that collides with a reserved keyword matches either interpretation (negative operators exclude both), and `labels.<key>` targets only the label. The response also carries the org's label pairs and the reserved filter keys for building filter suggestions.",
|
||||
RequestQuery: new(ruletypes.ListRulesParams),
|
||||
Response: new(ruletypes.ListableRules),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest},
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
})).Methods(http.MethodGet).GetError(); err != nil {
|
||||
return err
|
||||
@@ -132,64 +116,6 @@ func (provider *provider) addRulerRoutes(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/rule_views", handler.New(provider.authzMiddleware.ViewAccess(provider.rulerHandler.ListRuleViews), handler.OpenAPIDef{
|
||||
ID: "ListRuleViews",
|
||||
Tags: []string{"rules"},
|
||||
Summary: "List rule saved views",
|
||||
Description: "Returns every saved view in the calling user's org. Saved views are shared org-wide.",
|
||||
Response: new(ruletypes.ListableRuleViews),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
})).Methods(http.MethodGet).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/rule_views", handler.New(provider.authzMiddleware.ViewAccess(provider.rulerHandler.CreateRuleView), handler.OpenAPIDef{
|
||||
ID: "CreateRuleView",
|
||||
Tags: []string{"rules"},
|
||||
Summary: "Create rule saved view",
|
||||
Description: "Persists the calling user's rule listing state (query, states, sort, order) as a named, reusable view shared across the org.",
|
||||
Request: new(ruletypes.PostableRuleView),
|
||||
RequestContentType: "application/json",
|
||||
Response: new(ruletypes.RuleView),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusCreated,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest},
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
})).Methods(http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/rule_views/{id}", handler.New(provider.authzMiddleware.ViewAccess(provider.rulerHandler.UpdateRuleView), handler.OpenAPIDef{
|
||||
ID: "UpdateRuleView",
|
||||
Tags: []string{"rules"},
|
||||
Summary: "Update rule saved view",
|
||||
Description: "Replaces a saved view's name and data. Saved views are shared org-wide.",
|
||||
Request: new(ruletypes.UpdatableRuleView),
|
||||
RequestContentType: "application/json",
|
||||
Response: new(ruletypes.RuleView),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
})).Methods(http.MethodPut).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/rule_views/{id}", handler.New(provider.authzMiddleware.ViewAccess(provider.rulerHandler.DeleteRuleView), handler.OpenAPIDef{
|
||||
ID: "DeleteRuleView",
|
||||
Tags: []string{"rules"},
|
||||
Summary: "Delete rule saved view",
|
||||
Description: "Removes a saved view. Saved views are shared org-wide. Deleting a non-existent view returns 404.",
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
})).Methods(http.MethodDelete).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/downtime_schedules", handler.New(provider.authzMiddleware.ViewAccess(provider.rulerHandler.ListDowntimeSchedules), handler.OpenAPIDef{
|
||||
ID: "ListDowntimeSchedules",
|
||||
Tags: []string{"downtimeschedules"},
|
||||
|
||||
@@ -19,12 +19,6 @@ type Module interface {
|
||||
// gets the public sharing config for the dashboard
|
||||
GetPublic(context.Context, valuer.UUID, valuer.UUID) (*dashboardtypes.PublicDashboard, error)
|
||||
|
||||
// get the dashboard data by public dashboard id
|
||||
GetDashboardByPublicID(context.Context, valuer.UUID) (*dashboardtypes.Dashboard, error)
|
||||
|
||||
// gets the query results by widget index and public shared id for a dashboard
|
||||
GetPublicWidgetQueryRange(context.Context, valuer.UUID, uint64, uint64, uint64) (*querybuildertypesv5.QueryRangeResponse, error)
|
||||
|
||||
// gets the selectors and org for the given public dashboard
|
||||
GetPublicDashboardSelectorsAndOrg(context.Context, valuer.UUID, []*types.Organization) ([]coretypes.Selector, valuer.UUID, error)
|
||||
|
||||
@@ -34,23 +28,6 @@ type Module interface {
|
||||
// deletes the public sharing config and disables public sharing for the dashboard
|
||||
DeletePublic(context.Context, valuer.UUID, valuer.UUID) error
|
||||
|
||||
Create(ctx context.Context, orgID valuer.UUID, createdBy string, creator valuer.UUID, source dashboardtypes.Source, data dashboardtypes.PostableDashboard) (*dashboardtypes.Dashboard, error)
|
||||
|
||||
Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*dashboardtypes.Dashboard, error)
|
||||
|
||||
List(ctx context.Context, orgID valuer.UUID) ([]*dashboardtypes.Dashboard, error)
|
||||
|
||||
Update(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, data dashboardtypes.UpdatableDashboard, diff int) (*dashboardtypes.Dashboard, error)
|
||||
|
||||
LockUnlock(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, isAdmin bool, lock bool) error
|
||||
|
||||
Delete(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error
|
||||
|
||||
// DeleteUnsafe deletes a dashboard bypassing the guards. Intended for internal system callers.
|
||||
DeleteUnsafe(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error
|
||||
|
||||
GetByMetricNames(ctx context.Context, orgID valuer.UUID, metricNames []string) (map[string][]dashboardtypes.DashboardPanelRef, error)
|
||||
|
||||
statsreporter.StatsCollector
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
@@ -118,10 +95,6 @@ type Handler interface {
|
||||
|
||||
GetPublic(http.ResponseWriter, *http.Request)
|
||||
|
||||
GetPublicData(http.ResponseWriter, *http.Request)
|
||||
|
||||
GetPublicWidgetQueryRange(http.ResponseWriter, *http.Request)
|
||||
|
||||
GetPublicDataV2(http.ResponseWriter, *http.Request)
|
||||
|
||||
GetPublicWidgetQueryRangeV2(http.ResponseWriter, *http.Request)
|
||||
@@ -130,14 +103,6 @@ type Handler interface {
|
||||
|
||||
DeletePublic(http.ResponseWriter, *http.Request)
|
||||
|
||||
Create(http.ResponseWriter, *http.Request)
|
||||
|
||||
Update(http.ResponseWriter, *http.Request)
|
||||
|
||||
LockUnlock(http.ResponseWriter, *http.Request)
|
||||
|
||||
Delete(http.ResponseWriter, *http.Request)
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
// v2 dashboard methods
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -3,11 +3,9 @@ package impldashboard
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/authz"
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/http/binding"
|
||||
"github.com/SigNoz/signoz/pkg/http/render"
|
||||
@@ -29,22 +27,6 @@ func NewHandler(module dashboard.Module, providerSettings factory.ProviderSettin
|
||||
return &handler{module: module, providerSettings: providerSettings, authz: authz}
|
||||
}
|
||||
|
||||
func (handler *handler) Create(rw http.ResponseWriter, r *http.Request) {
|
||||
render.Error(rw, dashboardtypes.NewV1DeprecatedError("create a dashboard with POST /api/v2/dashboards"))
|
||||
}
|
||||
|
||||
func (handler *handler) Update(rw http.ResponseWriter, r *http.Request) {
|
||||
render.Error(rw, dashboardtypes.NewV1DeprecatedError("update a dashboard with PUT /api/v2/dashboards/{id}, or patch it with PATCH /api/v2/dashboards/{id}"))
|
||||
}
|
||||
|
||||
func (handler *handler) LockUnlock(rw http.ResponseWriter, r *http.Request) {
|
||||
render.Error(rw, dashboardtypes.NewV1DeprecatedError("lock a dashboard with PUT /api/v2/dashboards/{id}/lock, or unlock it with DELETE /api/v2/dashboards/{id}/lock"))
|
||||
}
|
||||
|
||||
func (handler *handler) Delete(rw http.ResponseWriter, r *http.Request) {
|
||||
render.Error(rw, dashboardtypes.NewV1DeprecatedError("delete a dashboard with DELETE /api/v2/dashboards/{id}"))
|
||||
}
|
||||
|
||||
func (handler *handler) CreatePublic(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
@@ -61,7 +43,7 @@ func (handler *handler) CreatePublic(rw http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
_, err = handler.module.Get(ctx, valuer.MustNewUUID(claims.OrgID), id)
|
||||
_, err = handler.module.GetV2(ctx, valuer.MustNewUUID(claims.OrgID), id)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
@@ -99,7 +81,7 @@ func (handler *handler) GetPublic(rw http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
_, err = handler.module.Get(ctx, valuer.MustNewUUID(claims.OrgID), id)
|
||||
_, err = handler.module.GetV2(ctx, valuer.MustNewUUID(claims.OrgID), id)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
@@ -114,107 +96,6 @@ func (handler *handler) GetPublic(rw http.ResponseWriter, r *http.Request) {
|
||||
render.Success(rw, http.StatusOK, dashboardtypes.NewGettablePublicDashboard(publicDashboard))
|
||||
}
|
||||
|
||||
func (handler *handler) GetPublicData(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
id, err := valuer.NewUUID(mux.Vars(r)["id"])
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
dashboard, err := handler.module.GetDashboardByPublicID(ctx, id)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
publicDashboard, err := handler.module.GetPublic(ctx, dashboard.OrgID, valuer.MustNewUUID(dashboard.ID))
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
gettablePublicDashboardData, err := dashboardtypes.NewPublicDashboardDataFromDashboard(dashboard, publicDashboard)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusOK, gettablePublicDashboardData)
|
||||
}
|
||||
|
||||
func (handler *handler) GetPublicWidgetQueryRange(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
id, err := valuer.NewUUID(mux.Vars(r)["id"])
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
widgetIndex, ok := mux.Vars(r)["idx"]
|
||||
if !ok {
|
||||
render.Error(rw, errors.New(errors.TypeInvalidInput, dashboardtypes.ErrCodePublicDashboardInvalidInput, "widget index is missing from the path"))
|
||||
return
|
||||
}
|
||||
|
||||
dashboard, err := handler.module.GetDashboardByPublicID(ctx, id)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
publicDashboard, err := handler.module.GetPublic(ctx, dashboard.OrgID, valuer.MustNewUUID(dashboard.ID))
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
widgetIdx, err := strconv.ParseUint(widgetIndex, 10, 64)
|
||||
if err != nil {
|
||||
render.Error(rw, errors.New(errors.TypeInvalidInput, dashboardtypes.ErrCodePublicDashboardInvalidInput, "invalid widget index"))
|
||||
return
|
||||
}
|
||||
|
||||
var startTime, endTime uint64
|
||||
if publicDashboard.TimeRangeEnabled {
|
||||
startTimeUint, err := strconv.ParseUint(r.URL.Query().Get("startTime"), 10, 64)
|
||||
if err != nil {
|
||||
render.Error(rw, errors.New(errors.TypeInvalidInput, dashboardtypes.ErrCodePublicDashboardInvalidInput, "invalid startTime"))
|
||||
return
|
||||
}
|
||||
|
||||
endTimeUint, err := strconv.ParseUint(r.URL.Query().Get("endTime"), 10, 64)
|
||||
if err != nil {
|
||||
render.Error(rw, errors.New(errors.TypeInvalidInput, dashboardtypes.ErrCodePublicDashboardInvalidInput, "invalid endTime"))
|
||||
return
|
||||
}
|
||||
|
||||
startTime = startTimeUint
|
||||
endTime = endTimeUint
|
||||
} else {
|
||||
timeRange, err := time.ParseDuration(publicDashboard.DefaultTimeRange)
|
||||
if err != nil {
|
||||
// this should't happen as we shouldn't let such values in DB
|
||||
panic(err)
|
||||
}
|
||||
|
||||
startTime = uint64(time.Now().Add(-timeRange).UnixMilli())
|
||||
endTime = uint64(time.Now().UnixMilli())
|
||||
}
|
||||
|
||||
queryRangeResults, err := handler.module.GetPublicWidgetQueryRange(ctx, id, widgetIdx, startTime, endTime)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusOK, queryRangeResults)
|
||||
}
|
||||
|
||||
func (handler *handler) UpdatePublic(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
@@ -231,7 +112,7 @@ func (handler *handler) UpdatePublic(rw http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
_, err = handler.module.Get(ctx, valuer.MustNewUUID(claims.OrgID), id)
|
||||
_, err = handler.module.GetV2(ctx, valuer.MustNewUUID(claims.OrgID), id)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
@@ -275,7 +156,7 @@ func (handler *handler) DeletePublic(rw http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
_, err = handler.module.Get(ctx, valuer.MustNewUUID(claims.OrgID), id)
|
||||
_, err = handler.module.GetV2(ctx, valuer.MustNewUUID(claims.OrgID), id)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
|
||||
@@ -2,8 +2,6 @@ package impldashboard
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"slices"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/analytics"
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
@@ -11,7 +9,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/modules/dashboard"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||
"github.com/SigNoz/signoz/pkg/modules/tag"
|
||||
"github.com/SigNoz/signoz/pkg/querybuilder"
|
||||
"github.com/SigNoz/signoz/pkg/queryparser"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
@@ -42,194 +39,6 @@ func NewModule(store dashboardtypes.Store, settings factory.ProviderSettings, an
|
||||
systemDashboardRegistry: systemDashboardRegistry,
|
||||
}
|
||||
}
|
||||
|
||||
func (module *module) Create(ctx context.Context, orgID valuer.UUID, createdBy string, creator valuer.UUID, source dashboardtypes.Source, postableDashboard dashboardtypes.PostableDashboard) (*dashboardtypes.Dashboard, error) {
|
||||
dashboard, err := dashboardtypes.NewDashboard(orgID, createdBy, source, postableDashboard)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
storableDashboard, err := dashboardtypes.NewStorableDashboardFromDashboard(dashboard)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = module.store.Create(ctx, storableDashboard)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
module.analytics.TrackUser(ctx, orgID.String(), creator.String(), "Dashboard Created", dashboardtypes.NewStatsFromStorableDashboards([]*dashboardtypes.StorableDashboard{storableDashboard}))
|
||||
return dashboard, nil
|
||||
}
|
||||
|
||||
func (module *module) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*dashboardtypes.Dashboard, error) {
|
||||
storableDashboard, err := module.store.Get(ctx, orgID, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dashboardtypes.NewDashboardFromStorableDashboard(storableDashboard), nil
|
||||
}
|
||||
|
||||
func (module *module) List(ctx context.Context, orgID valuer.UUID) ([]*dashboardtypes.Dashboard, error) {
|
||||
storableDashboards, err := module.store.List(ctx, orgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// system dashboards are hidden from the listing endpoint but still gettable by id.
|
||||
filtered := make([]*dashboardtypes.StorableDashboard, 0, len(storableDashboards))
|
||||
for _, storable := range storableDashboards {
|
||||
if storable.Source == dashboardtypes.SourceSystem {
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, storable)
|
||||
}
|
||||
|
||||
return dashboardtypes.NewDashboardsFromStorableDashboards(filtered), nil
|
||||
}
|
||||
|
||||
func (module *module) Update(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, updatableDashboard dashboardtypes.UpdatableDashboard, diff int) (*dashboardtypes.Dashboard, error) {
|
||||
dashboard, err := module.Get(ctx, orgID, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := dashboard.ErrIfNotMutable(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = dashboard.Update(ctx, updatableDashboard, updatedBy, diff)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
storableDashboard, err := dashboardtypes.NewStorableDashboardFromDashboard(dashboard)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = module.store.Update(ctx, orgID, storableDashboard)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dashboard, nil
|
||||
}
|
||||
|
||||
func (module *module) LockUnlock(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, isAdmin bool, lock bool) error {
|
||||
dashboard, err := module.Get(ctx, orgID, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := dashboard.ErrIfNotLockable(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = dashboard.LockUnlock(lock, isAdmin, updatedBy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
storableDashboard, err := dashboardtypes.NewStorableDashboardFromDashboard(dashboard)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = module.store.Update(ctx, orgID, storableDashboard)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (module *module) Delete(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error {
|
||||
dashboard, err := module.Get(ctx, orgID, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := dashboard.ErrIfNotDeletable(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if dashboard.Locked {
|
||||
return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "dashboard is locked, please unlock the dashboard to be delete it")
|
||||
}
|
||||
|
||||
err = module.store.Delete(ctx, orgID, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (module *module) DeleteUnsafe(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error {
|
||||
return module.store.Delete(ctx, orgID, id)
|
||||
}
|
||||
|
||||
func (module *module) GetByMetricNames(ctx context.Context, orgID valuer.UUID, metricNames []string) (map[string][]dashboardtypes.DashboardPanelRef, error) {
|
||||
dashboards, err := module.List(ctx, orgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make(map[string][]dashboardtypes.DashboardPanelRef)
|
||||
|
||||
for _, dashboard := range dashboards {
|
||||
dashData := dashboard.Data
|
||||
dashTitle, _ := dashData["title"].(string)
|
||||
widgets, ok := dashData["widgets"].([]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, w := range widgets {
|
||||
widget, ok := w.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
widgetTitle, _ := widget["title"].(string)
|
||||
widgetID, _ := widget["id"].(string)
|
||||
|
||||
query, ok := widget["query"].(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// Track which metrics were found in this widget, along with the
|
||||
// group-by and filter labels referenced for each metric. CH/PromQL
|
||||
// paths are presence-only and leave the label sets empty.
|
||||
foundMetrics := make(map[string]bool)
|
||||
groupByByMetric := make(map[string][]string)
|
||||
filterByByMetric := make(map[string][]string)
|
||||
|
||||
// Check all three query types
|
||||
module.checkBuilderQueriesForMetricNames(query, metricNames, foundMetrics, groupByByMetric, filterByByMetric)
|
||||
module.checkClickHouseQueriesForMetricNames(ctx, query, metricNames, foundMetrics)
|
||||
module.checkPromQLQueriesForMetricNames(ctx, query, metricNames, foundMetrics)
|
||||
|
||||
// Add widget to results for all found metrics
|
||||
for metricName := range foundMetrics {
|
||||
result[metricName] = append(result[metricName], dashboardtypes.DashboardPanelRef{
|
||||
DashboardID: dashboard.ID,
|
||||
DashboardName: dashTitle,
|
||||
PanelID: widgetID,
|
||||
PanelName: widgetTitle,
|
||||
GroupBy: groupByByMetric[metricName],
|
||||
FilterBy: filterByByMetric[metricName],
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (module *module) Collect(ctx context.Context, orgID valuer.UUID) (map[string]any, error) {
|
||||
dashboards, err := module.store.List(ctx, orgID)
|
||||
if err != nil {
|
||||
@@ -248,14 +57,6 @@ func (module *module) GetPublic(_ context.Context, _, _ valuer.UUID) (*dashboard
|
||||
return nil, errors.Newf(errors.TypeUnsupported, dashboardtypes.ErrCodePublicDashboardUnsupported, "not implemented")
|
||||
}
|
||||
|
||||
func (module *module) GetDashboardByPublicID(_ context.Context, _ valuer.UUID) (*dashboardtypes.Dashboard, error) {
|
||||
return nil, errors.Newf(errors.TypeUnsupported, dashboardtypes.ErrCodePublicDashboardUnsupported, "not implemented")
|
||||
}
|
||||
|
||||
func (module *module) GetPublicWidgetQueryRange(context.Context, valuer.UUID, uint64, uint64, uint64) (*qbtypes.QueryRangeResponse, error) {
|
||||
return nil, errors.Newf(errors.TypeUnsupported, dashboardtypes.ErrCodePublicDashboardUnsupported, "not implemented")
|
||||
}
|
||||
|
||||
func (module *module) GetDashboardByPublicIDV2(_ context.Context, _ valuer.UUID) (*dashboardtypes.DashboardV2, error) {
|
||||
return nil, errors.Newf(errors.TypeUnsupported, dashboardtypes.ErrCodePublicDashboardUnsupported, "not implemented")
|
||||
}
|
||||
@@ -275,213 +76,3 @@ func (module *module) UpdatePublic(_ context.Context, _ valuer.UUID, _ *dashboar
|
||||
func (module *module) DeletePublic(_ context.Context, _ valuer.UUID, _ valuer.UUID) error {
|
||||
return errors.Newf(errors.TypeUnsupported, dashboardtypes.ErrCodePublicDashboardUnsupported, "not implemented")
|
||||
}
|
||||
|
||||
// checkBuilderQueriesForMetricNames checks builder.queryData[] for aggregations[].metricName.
|
||||
// For each queryData entry whose dataSource is "metrics" and that references a
|
||||
// target metric, it accumulates (deduped) the group-by and filter labels used
|
||||
// by that entry into groupByByMetric/filterByByMetric, keyed by metric name.
|
||||
func (module *module) checkBuilderQueriesForMetricNames(query map[string]interface{}, metricNames []string, foundMetrics map[string]bool, groupByByMetric, filterByByMetric map[string][]string) {
|
||||
builder, ok := query["builder"].(map[string]interface{})
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
queryData, ok := builder["queryData"].([]interface{})
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
for _, qd := range queryData {
|
||||
data, ok := qd.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check dataSource is metrics
|
||||
if dataSource, ok := data["dataSource"].(string); !ok || dataSource != "metrics" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check aggregations[].metricName
|
||||
aggregations, ok := data["aggregations"].([]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
entryMetrics := make([]string, 0, len(aggregations))
|
||||
for _, agg := range aggregations {
|
||||
aggMap, ok := agg.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
metricName, ok := aggMap["metricName"].(string)
|
||||
if !ok || metricName == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if slices.Contains(metricNames, metricName) {
|
||||
foundMetrics[metricName] = true
|
||||
entryMetrics = append(entryMetrics, metricName)
|
||||
}
|
||||
}
|
||||
|
||||
if len(entryMetrics) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
groupBy := extractBuilderGroupByLabels(data)
|
||||
filterBy := extractBuilderFilterLabels(data)
|
||||
for _, metricName := range entryMetrics {
|
||||
groupByByMetric[metricName] = appendDedup(groupByByMetric[metricName], groupBy...)
|
||||
filterByByMetric[metricName] = appendDedup(filterByByMetric[metricName], filterBy...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func extractBuilderGroupByLabels(data map[string]interface{}) []string {
|
||||
gb, ok := data["groupBy"].([]interface{})
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
out := make([]string, 0, len(gb))
|
||||
for _, g := range gb {
|
||||
gm, ok := g.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if name, ok := gm["name"].(string); ok && name != "" {
|
||||
out = append(out, name)
|
||||
continue
|
||||
}
|
||||
// v3: groupBy[].key may be a plain string ...
|
||||
if key, ok := gm["key"].(string); ok && key != "" {
|
||||
out = append(out, key)
|
||||
continue
|
||||
}
|
||||
// ... or a nested object {key: "<name>"}.
|
||||
if km, ok := gm["key"].(map[string]interface{}); ok {
|
||||
if key, ok := km["key"].(string); ok && key != "" {
|
||||
out = append(out, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func extractBuilderFilterLabels(data map[string]interface{}) []string {
|
||||
out := []string{}
|
||||
|
||||
// v5: filter.expression
|
||||
if f, ok := data["filter"].(map[string]interface{}); ok {
|
||||
if expr, ok := f["expression"].(string); ok && expr != "" {
|
||||
for _, sel := range querybuilder.QueryStringToKeysSelectors(expr) {
|
||||
if sel != nil && sel.Name != "" {
|
||||
out = append(out, sel.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// v3: filters.items[].key.key
|
||||
if f, ok := data["filters"].(map[string]interface{}); ok {
|
||||
if items, ok := f["items"].([]interface{}); ok {
|
||||
for _, it := range items {
|
||||
im, ok := it.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
km, ok := im["key"].(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if key, ok := km["key"].(string); ok && key != "" {
|
||||
out = append(out, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func appendDedup(dst []string, values ...string) []string {
|
||||
for _, v := range values {
|
||||
if v == "" || slices.Contains(dst, v) {
|
||||
continue
|
||||
}
|
||||
dst = append(dst, v)
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// checkClickHouseQueriesForMetricNames checks clickhouse_sql[] array for metric names in query strings.
|
||||
func (module *module) checkClickHouseQueriesForMetricNames(ctx context.Context, query map[string]interface{}, metricNames []string, foundMetrics map[string]bool) {
|
||||
clickhouseSQL, ok := query["clickhouse_sql"].([]interface{})
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
for _, chQuery := range clickhouseSQL {
|
||||
chQueryMap, ok := chQuery.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
queryStr, ok := chQueryMap["query"].(string)
|
||||
if !ok || queryStr == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse query to extract metric names
|
||||
result, err := module.queryParser.AnalyzeQueryFilter(ctx, qbtypes.QueryTypeClickHouseSQL, queryStr)
|
||||
if err != nil {
|
||||
// Log warning and continue - parsing errors shouldn't break the search
|
||||
module.settings.Logger().WarnContext(ctx, "failed to parse ClickHouse query", slog.String("query", queryStr), errors.Attr(err))
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if any of the search metric names are in the extracted metric names
|
||||
for _, metricName := range metricNames {
|
||||
if slices.Contains(result.MetricNames, metricName) {
|
||||
foundMetrics[metricName] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// checkPromQLQueriesForMetricNames checks promql[] array for metric names in query strings.
|
||||
func (module *module) checkPromQLQueriesForMetricNames(ctx context.Context, query map[string]interface{}, metricNames []string, foundMetrics map[string]bool) {
|
||||
promQL, ok := query["promql"].([]interface{})
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
for _, promQuery := range promQL {
|
||||
promQueryMap, ok := promQuery.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
queryStr, ok := promQueryMap["query"].(string)
|
||||
if !ok || queryStr == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse query to extract metric names
|
||||
result, err := module.queryParser.AnalyzeQueryFilter(ctx, qbtypes.QueryTypePromQL, queryStr)
|
||||
if err != nil {
|
||||
// Log warning and continue - parsing errors shouldn't break the search
|
||||
module.settings.Logger().WarnContext(ctx, "failed to parse PromQL query", slog.String("query", queryStr), errors.Attr(err))
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if any of the search metric names are in the extracted metric names
|
||||
for _, metricName := range metricNames {
|
||||
if slices.Contains(result.MetricNames, metricName) {
|
||||
foundMetrics[metricName] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,38 +196,6 @@ func (h *handler) GetMetricAlerts(rw http.ResponseWriter, req *http.Request) {
|
||||
render.Success(rw, http.StatusOK, out)
|
||||
}
|
||||
|
||||
func (h *handler) GetMetricDashboards(rw http.ResponseWriter, req *http.Request) {
|
||||
claims, err := authtypes.ClaimsFromContext(req.Context())
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
var in metricsexplorertypes.MetricNameQuery
|
||||
if err := binding.Query.BindQuery(req.URL.Query(), &in); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
if err := in.Validate(); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
orgID := valuer.MustNewUUID(claims.OrgID)
|
||||
|
||||
if err := h.checkMetricExists(req.Context(), orgID, in.MetricName); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
out, err := h.module.GetMetricDashboards(req.Context(), orgID, in.MetricName)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
render.Success(rw, http.StatusOK, out)
|
||||
}
|
||||
|
||||
func (h *handler) GetMetricDashboardsV2(rw http.ResponseWriter, req *http.Request) {
|
||||
claims, err := authtypes.ClaimsFromContext(req.Context())
|
||||
if err != nil {
|
||||
|
||||
@@ -23,7 +23,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/telemetryschema/metricstelemetryschema"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrystore"
|
||||
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/featuretypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/instrumentationtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/metricsexplorertypes"
|
||||
@@ -390,18 +389,6 @@ func (m *module) GetMetricAlerts(ctx context.Context, orgID valuer.UUID, metricN
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *module) GetMetricDashboards(ctx context.Context, orgID valuer.UUID, metricName string) (*metricsexplorertypes.MetricDashboardsResponse, error) {
|
||||
if metricName == "" {
|
||||
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "metricName is required")
|
||||
}
|
||||
data, err := m.dashboardModule.GetByMetricNames(ctx, orgID, []string{metricName})
|
||||
if err != nil {
|
||||
return nil, errors.WrapInternalf(err, errors.CodeInternal, "failed to get dashboards for metric")
|
||||
}
|
||||
|
||||
return newMetricDashboardsResponse(data[metricName]), nil
|
||||
}
|
||||
|
||||
func (m *module) GetMetricDashboardsV2(ctx context.Context, orgID valuer.UUID, metricName string) (*metricsexplorertypes.MetricDashboardPanelsResponse, error) {
|
||||
if metricName == "" {
|
||||
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "metricName is required")
|
||||
@@ -414,22 +401,6 @@ func (m *module) GetMetricDashboardsV2(ctx context.Context, orgID valuer.UUID, m
|
||||
return metricsexplorertypes.NewMetricDashboardPanelsResponse(data[metricName]), nil
|
||||
}
|
||||
|
||||
func newMetricDashboardsResponse(dashboardList []dashboardtypes.DashboardPanelRef) *metricsexplorertypes.MetricDashboardsResponse {
|
||||
dashboards := make([]metricsexplorertypes.MetricDashboard, 0, len(dashboardList))
|
||||
for _, item := range dashboardList {
|
||||
dashboards = append(dashboards, metricsexplorertypes.MetricDashboard{
|
||||
DashboardName: item.DashboardName,
|
||||
DashboardID: item.DashboardID,
|
||||
WidgetID: item.PanelID,
|
||||
WidgetName: item.PanelName,
|
||||
})
|
||||
}
|
||||
|
||||
return &metricsexplorertypes.MetricDashboardsResponse{
|
||||
Dashboards: dashboards,
|
||||
}
|
||||
}
|
||||
|
||||
// GetMetricHighlights returns highlights for a metric including data points, last received, total time series, and active time series.
|
||||
func (m *module) GetMetricHighlights(ctx context.Context, orgID valuer.UUID, metricName string) (*metricsexplorertypes.MetricHighlightsResponse, error) {
|
||||
if metricName == "" {
|
||||
|
||||
@@ -17,7 +17,6 @@ type Handler interface {
|
||||
GetMetricAttributes(http.ResponseWriter, *http.Request)
|
||||
UpdateMetricMetadata(http.ResponseWriter, *http.Request)
|
||||
GetMetricAlerts(http.ResponseWriter, *http.Request)
|
||||
GetMetricDashboards(http.ResponseWriter, *http.Request)
|
||||
GetMetricDashboardsV2(http.ResponseWriter, *http.Request)
|
||||
GetMetricHighlights(http.ResponseWriter, *http.Request)
|
||||
GetOnboardingStatus(http.ResponseWriter, *http.Request)
|
||||
@@ -33,7 +32,6 @@ type Module interface {
|
||||
GetMetricMetadataMulti(ctx context.Context, orgID valuer.UUID, metricNames []string) (map[string]*metricsexplorertypes.MetricMetadata, error)
|
||||
UpdateMetricMetadata(ctx context.Context, orgID valuer.UUID, req *metricsexplorertypes.UpdateMetricMetadataRequest) error
|
||||
GetMetricAlerts(ctx context.Context, orgID valuer.UUID, metricName string) (*metricsexplorertypes.MetricAlertsResponse, error)
|
||||
GetMetricDashboards(ctx context.Context, orgID valuer.UUID, metricName string) (*metricsexplorertypes.MetricDashboardsResponse, error)
|
||||
GetMetricDashboardsV2(ctx context.Context, orgID valuer.UUID, metricName string) (*metricsexplorertypes.MetricDashboardPanelsResponse, error)
|
||||
GetMetricHighlights(ctx context.Context, orgID valuer.UUID, metricName string) (*metricsexplorertypes.MetricHighlightsResponse, error)
|
||||
GetMetricAttributes(ctx context.Context, orgID valuer.UUID, req *metricsexplorertypes.MetricAttributesRequest) (*metricsexplorertypes.MetricAttributesResponse, error)
|
||||
|
||||
@@ -58,7 +58,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/featuretypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/instrumentationtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/licensetypes"
|
||||
@@ -376,12 +375,6 @@ func (aH *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
|
||||
router.HandleFunc("/api/v1/rules/{id}/history/top_contributors", am.ViewAccess(aH.getRuleStateHistoryTopContributors)).Methods(http.MethodPost)
|
||||
router.HandleFunc("/api/v1/rules/{id}/history/overall_status", am.ViewAccess(aH.getOverallStateTransitions)).Methods(http.MethodPost)
|
||||
|
||||
router.HandleFunc("/api/v1/dashboards", am.ViewAccess(aH.List)).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/v1/dashboards", am.EditAccess(aH.Signoz.Handlers.Dashboard.Create)).Methods(http.MethodPost)
|
||||
router.HandleFunc("/api/v1/dashboards/{id}", am.ViewAccess(aH.Get)).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/v1/dashboards/{id}", am.EditAccess(aH.Signoz.Handlers.Dashboard.Update)).Methods(http.MethodPut)
|
||||
router.HandleFunc("/api/v1/dashboards/{id}", am.EditAccess(aH.Signoz.Handlers.Dashboard.Delete)).Methods(http.MethodDelete)
|
||||
router.HandleFunc("/api/v1/dashboards/{id}/lock", am.EditAccess(aH.Signoz.Handlers.Dashboard.LockUnlock)).Methods(http.MethodPut)
|
||||
router.HandleFunc("/api/v2/variables/query", am.ViewAccess(aH.queryDashboardVarsV2)).Methods(http.MethodPost)
|
||||
|
||||
router.HandleFunc("/api/v1/explorer/views", am.ViewAccess(aH.Signoz.Handlers.SavedView.List)).Methods(http.MethodGet)
|
||||
@@ -946,14 +939,6 @@ func prepareQuery(r *http.Request) (string, error) {
|
||||
return newQuery, nil
|
||||
}
|
||||
|
||||
func (aH *APIHandler) Get(rw http.ResponseWriter, r *http.Request) {
|
||||
render.Error(rw, dashboardtypes.NewV1DeprecatedError("get a dashboard with GET /api/v2/dashboards/{id}"))
|
||||
}
|
||||
|
||||
func (aH *APIHandler) List(rw http.ResponseWriter, r *http.Request) {
|
||||
render.Error(rw, dashboardtypes.NewV1DeprecatedError("list dashboards with GET /api/v2/dashboards"))
|
||||
}
|
||||
|
||||
func (aH *APIHandler) queryDashboardVarsV2(w http.ResponseWriter, r *http.Request) {
|
||||
query, err := prepareQuery(r)
|
||||
if err != nil {
|
||||
|
||||
@@ -851,8 +851,6 @@ func (m *Manager) ListRuleStates(ctx context.Context) (*ruletypes.GettableRules,
|
||||
// initiate response object
|
||||
resp := make([]*ruletypes.GettableRule, 0)
|
||||
|
||||
stateByRuleID := m.snapshotRuleStates()
|
||||
|
||||
for _, s := range storedRules {
|
||||
|
||||
ruleResponse := ruletypes.GettableRule{}
|
||||
@@ -865,11 +863,11 @@ func (m *Manager) ListRuleStates(ctx context.Context) (*ruletypes.GettableRules,
|
||||
ruleResponse.Id = s.ID.StringValue()
|
||||
|
||||
// fetch state of rule from memory
|
||||
if state, ok := stateByRuleID[ruleResponse.Id]; !ok {
|
||||
if rm, ok := m.rules[ruleResponse.Id]; !ok {
|
||||
ruleResponse.State = ruletypes.StateDisabled
|
||||
ruleResponse.Disabled = true
|
||||
} else {
|
||||
ruleResponse.State = state
|
||||
ruleResponse.State = rm.State()
|
||||
}
|
||||
ruleResponse.CreatedAt = s.CreatedAt
|
||||
ruleResponse.CreatedBy = &s.CreatedBy
|
||||
@@ -881,66 +879,6 @@ func (m *Manager) ListRuleStates(ctx context.Context) (*ruletypes.GettableRules,
|
||||
return &ruletypes.GettableRules{Rules: resp}, nil
|
||||
}
|
||||
|
||||
// ListRules' total counts what is pageable after corrupt-row drops and the states filter.
|
||||
func (m *Manager) ListRules(ctx context.Context, params *ruletypes.ListRulesParams) (*ruletypes.ListableRules, error) {
|
||||
// validated here too, not just in the handler: non-API callers reach the manager directly
|
||||
if err := params.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
states, err := params.GetAlertStates()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stateFilter := make(map[ruletypes.AlertState]struct{}, len(states))
|
||||
for _, state := range states {
|
||||
stateFilter[state] = struct{}{}
|
||||
}
|
||||
|
||||
storedRules, err := m.ruleStore.GetStoredRulesMatching(ctx, claims.OrgID, params.Query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stateByRuleID := m.snapshotRuleStates()
|
||||
|
||||
listableRules, errByRuleID := ruletypes.NewListableRulesFromStorableRules(storedRules, stateByRuleID, stateFilter)
|
||||
for ruleID, err := range errByRuleID {
|
||||
m.logger.ErrorContext(ctx, "failed to unmarshal rule from db", slog.String("rule.id", ruleID), errors.Attr(err))
|
||||
}
|
||||
|
||||
total := int64(len(listableRules))
|
||||
ruletypes.SortListableRules(listableRules, params.Sort, params.Order)
|
||||
|
||||
start := min(params.Offset, len(listableRules))
|
||||
end := min(start+params.Limit, len(listableRules))
|
||||
currentPageRules := listableRules[start:end]
|
||||
|
||||
rawLabels, err := m.ruleStore.GetStoredRuleLabels(ctx, claims.OrgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
labelPairs := ruletypes.NewLabelPairsFromRawJSON(rawLabels, ruletypes.MaxListLabelPairs)
|
||||
|
||||
return ruletypes.NewListableRules(currentPageRules, total, labelPairs), nil
|
||||
}
|
||||
|
||||
func (m *Manager) snapshotRuleStates() map[string]ruletypes.AlertState {
|
||||
m.mtx.RLock()
|
||||
defer m.mtx.RUnlock()
|
||||
|
||||
states := make(map[string]ruletypes.AlertState, len(m.rules))
|
||||
for id, rule := range m.rules {
|
||||
states[id] = rule.State()
|
||||
}
|
||||
return states
|
||||
}
|
||||
|
||||
func (m *Manager) GetRule(ctx context.Context, id valuer.UUID) (*ruletypes.GettableRule, error) {
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
@@ -961,10 +899,7 @@ func (m *Manager) GetRule(ctx context.Context, id valuer.UUID) (*ruletypes.Getta
|
||||
}
|
||||
r.Id = id.StringValue()
|
||||
// fetch state of rule from memory
|
||||
m.mtx.RLock()
|
||||
rm, ok := m.rules[r.Id]
|
||||
m.mtx.RUnlock()
|
||||
if !ok {
|
||||
if rm, ok := m.rules[r.Id]; !ok {
|
||||
r.State = ruletypes.StateDisabled
|
||||
r.Disabled = true
|
||||
} else {
|
||||
|
||||
@@ -20,7 +20,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/telemetrystore/telemetrystoretest"
|
||||
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/metrictypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/ruletypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
@@ -29,17 +28,6 @@ import (
|
||||
cmock "github.com/SigNoz/clickhouse-go-mock"
|
||||
)
|
||||
|
||||
func TestManager_ListRules_ValidatesParams(t *testing.T) {
|
||||
m, err := NewManager(&ManagerOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = m.ListRules(context.Background(), &ruletypes.ListRulesParams{Limit: -1})
|
||||
require.ErrorContains(t, err, "invalid limit")
|
||||
|
||||
_, err = m.ListRules(context.Background(), &ruletypes.ListRulesParams{ListFilter: ruletypes.ListFilter{States: []string{"bogus"}}})
|
||||
require.ErrorContains(t, err, `invalid state "bogus"`)
|
||||
}
|
||||
|
||||
func TestManager_TestNotification_SendUnmatched_ThresholdRule(t *testing.T) {
|
||||
target := 10.0
|
||||
recovery := 5.0
|
||||
|
||||
@@ -4,7 +4,6 @@ import "net/http"
|
||||
|
||||
type Handler interface {
|
||||
ListRules(http.ResponseWriter, *http.Request)
|
||||
ListRulesV3(http.ResponseWriter, *http.Request)
|
||||
GetRuleByID(http.ResponseWriter, *http.Request)
|
||||
CreateRule(http.ResponseWriter, *http.Request)
|
||||
UpdateRuleByID(http.ResponseWriter, *http.Request)
|
||||
@@ -12,11 +11,6 @@ type Handler interface {
|
||||
PatchRuleByID(http.ResponseWriter, *http.Request)
|
||||
TestRule(http.ResponseWriter, *http.Request)
|
||||
|
||||
ListRuleViews(http.ResponseWriter, *http.Request)
|
||||
CreateRuleView(http.ResponseWriter, *http.Request)
|
||||
UpdateRuleView(http.ResponseWriter, *http.Request)
|
||||
DeleteRuleView(http.ResponseWriter, *http.Request)
|
||||
|
||||
ListDowntimeSchedules(http.ResponseWriter, *http.Request)
|
||||
GetDowntimeScheduleByID(http.ResponseWriter, *http.Request)
|
||||
CreateDowntimeSchedule(http.ResponseWriter, *http.Request)
|
||||
|
||||
@@ -17,9 +17,6 @@ type Ruler interface {
|
||||
// ListRuleStates returns all rules with their current evaluation state.
|
||||
ListRuleStates(ctx context.Context) (*ruletypes.GettableRules, error)
|
||||
|
||||
// ListRules returns a filtered, sorted page of rules with state, plus label pairs and reserved filter keys.
|
||||
ListRules(ctx context.Context, params *ruletypes.ListRulesParams) (*ruletypes.ListableRules, error)
|
||||
|
||||
// GetRule returns a single rule by ID.
|
||||
GetRule(ctx context.Context, id valuer.UUID) (*ruletypes.GettableRule, error)
|
||||
|
||||
@@ -49,9 +46,4 @@ type Ruler interface {
|
||||
// TODO: expose downtime CRUD as methods on Ruler directly instead of leaking the
|
||||
// store interface. The handler should not call store methods directly.
|
||||
MaintenanceStore() alertmanagertypes.MaintenanceStore
|
||||
|
||||
CreateRuleView(ctx context.Context, orgID valuer.UUID, postable ruletypes.PostableRuleView) (*ruletypes.RuleView, error)
|
||||
ListRuleViews(ctx context.Context, orgID valuer.UUID) (*ruletypes.ListableRuleViews, error)
|
||||
UpdateRuleView(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatable ruletypes.UpdatableRuleView) (*ruletypes.RuleView, error)
|
||||
DeleteRuleView(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error
|
||||
}
|
||||
|
||||
@@ -64,16 +64,6 @@ func (m *MockSQLRuleStore) GetStoredRules(ctx context.Context, orgID string) ([]
|
||||
return m.ruleStore.GetStoredRules(ctx, orgID)
|
||||
}
|
||||
|
||||
// GetStoredRulesMatching implements ruletypes.RuleStore - delegates to underlying ruleStore to trigger SQL.
|
||||
func (m *MockSQLRuleStore) GetStoredRulesMatching(ctx context.Context, orgID string, query string) ([]*ruletypes.StorableRule, error) {
|
||||
return m.ruleStore.GetStoredRulesMatching(ctx, orgID, query)
|
||||
}
|
||||
|
||||
// GetStoredRuleLabels implements ruletypes.RuleStore - delegates to underlying ruleStore to trigger SQL.
|
||||
func (m *MockSQLRuleStore) GetStoredRuleLabels(ctx context.Context, orgID string) ([]string, error) {
|
||||
return m.ruleStore.GetStoredRuleLabels(ctx, orgID)
|
||||
}
|
||||
|
||||
// GetStoredRulesByMetricName implements ruletypes.RuleStore - delegates to underlying ruleStore.
|
||||
func (m *MockSQLRuleStore) GetStoredRulesByMetricName(ctx context.Context, orgID string, metricName string) ([]ruletypes.RuleAlert, error) {
|
||||
return m.ruleStore.GetStoredRulesByMetricName(ctx, orgID, metricName)
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
package sqlrulestore
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/parser/filterquery/sqlcompiler"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/types/ruletypes"
|
||||
)
|
||||
|
||||
// Compile wraps compiler errors in the rules list filter error code.
|
||||
func Compile(query string, formatter sqlstore.SQLFormatter) (*sqlcompiler.Compiled, error) {
|
||||
compiled, errs := sqlcompiler.Compile(query, formatter, ruleFieldResolver{})
|
||||
if len(errs) > 0 {
|
||||
return nil, errors.NewInvalidInputf(ruletypes.ErrCodeRuleListFilterInvalid,
|
||||
"invalid filter query: %s", strings.Join(errs, "; "))
|
||||
}
|
||||
return compiled, nil
|
||||
}
|
||||
@@ -1,196 +0,0 @@
|
||||
package sqlrulestore
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
grammar "github.com/SigNoz/signoz/pkg/parser/filterquery/grammar"
|
||||
"github.com/SigNoz/signoz/pkg/parser/filterquery/sqlcompiler"
|
||||
qbtypesv5 "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/ruletypes"
|
||||
)
|
||||
|
||||
const (
|
||||
ruleDataColumn = "rule.data"
|
||||
ruleLabelsField = "labels"
|
||||
nameJSONPath = "$.alert"
|
||||
descriptionPath = "$.description"
|
||||
labelsJSONPath = "$.labels"
|
||||
alertTypePath = "$.alertType"
|
||||
ruleTypePath = "$.ruleType"
|
||||
)
|
||||
|
||||
// ruleFieldResolver maps rule list DSL keys; a non-reserved key is a case-sensitive label lookup.
|
||||
type ruleFieldResolver struct{}
|
||||
|
||||
func (r ruleFieldResolver) ResolveComparison(v *sqlcompiler.Visitor, rawKey string, operation qbtypesv5.FilterOperator, ctx *grammar.ComparisonContext) string {
|
||||
key := strings.ToLower(rawKey)
|
||||
|
||||
// labels.<key> is the explicit way to target only the label on a reserved-key collision.
|
||||
if strings.HasPrefix(key, ruletypes.DSLLabelsKeyPrefix) {
|
||||
labelKey := rawKey[len(ruletypes.DSLLabelsKeyPrefix):]
|
||||
if labelKey == "" {
|
||||
v.AddError("labels filter is missing a key, use labels.<key>")
|
||||
return ""
|
||||
}
|
||||
if _, allowed := ruletypes.LabelsKeyOps[operation]; !allowed {
|
||||
v.AddError("operator %s is not allowed on a labels.<key> filter", sqlcompiler.OperationName(operation))
|
||||
return ""
|
||||
}
|
||||
return r.labelComparison(v, ctx, operation, labelKey)
|
||||
}
|
||||
|
||||
allowedOperations, isReserved := ruletypes.ReservedOps[ruletypes.DSLKey(key)]
|
||||
_, labelAllowed := ruletypes.LabelsKeyOps[operation]
|
||||
|
||||
if !isReserved {
|
||||
if !labelAllowed {
|
||||
v.AddError("operator %s is not allowed on the label filter %q", sqlcompiler.OperationName(operation), rawKey)
|
||||
return ""
|
||||
}
|
||||
return r.labelComparison(v, ctx, operation, rawKey)
|
||||
}
|
||||
|
||||
_, reservedAllowed := allowedOperations[operation]
|
||||
// reserved severity is itself the severity-label lookup; an identical spelling would duplicate the predicate
|
||||
if ruletypes.DSLKey(key) == ruletypes.DSLKeySeverity && rawKey == string(ruletypes.DSLKeySeverity) {
|
||||
labelAllowed = false
|
||||
}
|
||||
|
||||
switch {
|
||||
case reservedAllowed && labelAllowed:
|
||||
reservedPredicate := r.resolveReservedKey(v, ctx, operation, ruletypes.DSLKey(key))
|
||||
labelPredicate := r.labelComparison(v, ctx, operation, rawKey)
|
||||
if reservedPredicate == "" || labelPredicate == "" {
|
||||
return ""
|
||||
}
|
||||
// the key matches both the reserved field and a same-named label; a negative term must exclude both
|
||||
if operation.IsNegativeOperator() {
|
||||
return v.Sb.And(reservedPredicate, labelPredicate)
|
||||
}
|
||||
return v.Sb.Or(reservedPredicate, labelPredicate)
|
||||
case reservedAllowed:
|
||||
return r.resolveReservedKey(v, ctx, operation, ruletypes.DSLKey(key))
|
||||
case labelAllowed:
|
||||
return r.labelComparison(v, ctx, operation, rawKey)
|
||||
default:
|
||||
v.AddError("operator %s is not allowed for key %q", sqlcompiler.OperationName(operation), key)
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (r ruleFieldResolver) resolveReservedKey(v *sqlcompiler.Visitor, ctx *grammar.ComparisonContext, operation qbtypesv5.FilterOperator, key ruletypes.DSLKey) string {
|
||||
switch key {
|
||||
case ruletypes.DSLKeyName:
|
||||
columnExpression := string(v.Formatter.JSONExtractString(ruleDataColumn, nameJSONPath))
|
||||
return v.BuildStringOperation(v.Sb, ctx, operation, columnExpression, string(key))
|
||||
case ruletypes.DSLKeySeverity:
|
||||
// severity is an alias for labels.severity, sharing its missing-label semantics.
|
||||
return r.labelComparison(v, ctx, operation, "severity")
|
||||
case ruletypes.DSLKeyCreatedBy:
|
||||
return v.BuildStringOperation(v.Sb, ctx, operation, "rule.created_by", string(key))
|
||||
case ruletypes.DSLKeyUpdatedBy:
|
||||
return v.BuildStringOperation(v.Sb, ctx, operation, "rule.updated_by", string(key))
|
||||
case ruletypes.DSLKeyCreatedAt:
|
||||
return v.BuildTimestampComparison(ctx, operation, "rule.created_at")
|
||||
case ruletypes.DSLKeyUpdatedAt:
|
||||
return v.BuildTimestampComparison(ctx, operation, "rule.updated_at")
|
||||
case ruletypes.DSLKeyAlertType:
|
||||
return r.enumComparison(v, ctx, operation, key, alertTypePath, alertTypeValues)
|
||||
case ruletypes.DSLKeyRuleType:
|
||||
return r.enumComparison(v, ctx, operation, key, ruleTypePath, ruleTypeValues)
|
||||
}
|
||||
v.AddError("no handler for reserved key %q", key)
|
||||
return ""
|
||||
}
|
||||
|
||||
// A missing label evaluates as the empty string for every value operator; EXISTS/NOT EXISTS test the raw extraction.
|
||||
func (ruleFieldResolver) labelComparison(v *sqlcompiler.Visitor, ctx *grammar.ComparisonContext, operation qbtypesv5.FilterOperator, labelKey string) string {
|
||||
columnExpression := string(v.Formatter.JSONExtractMapValue(ruleDataColumn, ruleLabelsField, labelKey))
|
||||
|
||||
switch operation {
|
||||
case qbtypesv5.FilterOperatorExists:
|
||||
return fmt.Sprintf("%s IS NOT NULL", columnExpression)
|
||||
case qbtypesv5.FilterOperatorNotExists:
|
||||
return fmt.Sprintf("%s IS NULL", columnExpression)
|
||||
}
|
||||
|
||||
keyForError := ruletypes.DSLLabelsKeyPrefix + labelKey
|
||||
columnExpression = fmt.Sprintf("COALESCE(%s, '')", columnExpression)
|
||||
return v.BuildStringOperation(v.Sb, ctx, operation, columnExpression, keyForError)
|
||||
}
|
||||
|
||||
func (ruleFieldResolver) enumComparison(v *sqlcompiler.Visitor, ctx *grammar.ComparisonContext, operation qbtypesv5.FilterOperator, key ruletypes.DSLKey, jsonPath string, allowedValues []string) string {
|
||||
columnExpression := string(v.Formatter.JSONExtractString(ruleDataColumn, jsonPath))
|
||||
|
||||
var values []string
|
||||
switch operation {
|
||||
case qbtypesv5.FilterOperatorEqual, qbtypesv5.FilterOperatorNotEqual:
|
||||
value, ok := v.ExtractSingleStringValue(ctx, string(key))
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
values = []string{value}
|
||||
case qbtypesv5.FilterOperatorIn, qbtypesv5.FilterOperatorNotIn:
|
||||
list, ok := v.ExtractStringValueList(ctx, string(key))
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
values = list
|
||||
default:
|
||||
v.AddError("operator %s on %q is not implemented", sqlcompiler.OperationName(operation), key)
|
||||
return ""
|
||||
}
|
||||
|
||||
for _, value := range values {
|
||||
if !slices.Contains(allowedValues, value) {
|
||||
v.AddError("invalid value %q for %q, expected one of: %s", value, key, strings.Join(allowedValues, ", "))
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
arguments := make([]any, len(values))
|
||||
for i, s := range values {
|
||||
arguments[i] = s
|
||||
}
|
||||
switch operation {
|
||||
case qbtypesv5.FilterOperatorEqual:
|
||||
return v.Sb.Equal(columnExpression, arguments[0])
|
||||
case qbtypesv5.FilterOperatorNotEqual:
|
||||
return v.Sb.NotEqual(columnExpression, arguments[0])
|
||||
case qbtypesv5.FilterOperatorNotIn:
|
||||
return v.Sb.NotIn(columnExpression, arguments...)
|
||||
default:
|
||||
return v.Sb.In(columnExpression, arguments...)
|
||||
}
|
||||
}
|
||||
|
||||
// ResolveFreeText searches name, description and the raw labels JSON (which also matches label keys).
|
||||
func (ruleFieldResolver) ResolveFreeText(v *sqlcompiler.Visitor, value string) string {
|
||||
nameColumn := string(v.Formatter.JSONExtractString(ruleDataColumn, nameJSONPath))
|
||||
descriptionColumn := string(v.Formatter.JSONExtractString(ruleDataColumn, descriptionPath))
|
||||
labelsColumn := string(v.Formatter.JSONExtractString(ruleDataColumn, labelsJSONPath))
|
||||
|
||||
return v.Sb.Or(
|
||||
v.BuildFreeTextContains(v.Sb, nameColumn, value),
|
||||
v.BuildFreeTextContains(v.Sb, descriptionColumn, value),
|
||||
v.BuildFreeTextContains(v.Sb, labelsColumn, value),
|
||||
)
|
||||
}
|
||||
|
||||
var alertTypeValues = func() []string {
|
||||
values := make([]string, 0, 4)
|
||||
for _, value := range (ruletypes.AlertType("")).Enum() {
|
||||
values = append(values, string(value.(ruletypes.AlertType)))
|
||||
}
|
||||
return values
|
||||
}()
|
||||
|
||||
var ruleTypeValues = func() []string {
|
||||
values := make([]string, 0, 3)
|
||||
for _, value := range (ruletypes.RuleType{}).Enum() {
|
||||
values = append(values, value.(ruletypes.RuleType).StringValue())
|
||||
}
|
||||
return values
|
||||
}()
|
||||
@@ -1,488 +0,0 @@
|
||||
package sqlrulestore
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore/sqlstoretest"
|
||||
"github.com/SigNoz/signoz/pkg/types/ruletypes"
|
||||
)
|
||||
|
||||
type compileCase struct {
|
||||
subtestName string
|
||||
dslQueryToCompile string
|
||||
emptyQueryExpected bool
|
||||
expectedSQL string
|
||||
expectedArgs []any
|
||||
expectedErrShouldContain string
|
||||
}
|
||||
|
||||
func runCompileCases(t *testing.T, cases []compileCase) {
|
||||
t.Helper()
|
||||
for _, c := range cases {
|
||||
t.Run(c.subtestName, func(t *testing.T) {
|
||||
out, err := Compile(c.dslQueryToCompile, formatter(t))
|
||||
|
||||
if c.expectedErrShouldContain != "" {
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, strings.ToLower(err.Error()), strings.ToLower(c.expectedErrShouldContain))
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
if c.emptyQueryExpected {
|
||||
assert.True(t, out.IsEmpty())
|
||||
return
|
||||
}
|
||||
require.NotNil(t, out)
|
||||
|
||||
if c.expectedSQL != "" {
|
||||
assert.Equal(t, normalizeSQL(c.expectedSQL), normalizeSQL(out.SQL))
|
||||
}
|
||||
if c.expectedArgs != nil {
|
||||
require.Len(t, out.Args, len(c.expectedArgs))
|
||||
for i, want := range c.expectedArgs {
|
||||
// Equal instants can differ in *Location, so compare via .Equal() instead of DeepEqual.
|
||||
if wantT, ok := want.(time.Time); ok {
|
||||
gotT, ok := out.Args[i].(time.Time)
|
||||
require.True(t, ok, "arg[%d]: want time.Time, got %T", i, out.Args[i])
|
||||
assert.True(t, wantT.Equal(gotT), "arg[%d]: want %s, got %s", i, wantT, gotT)
|
||||
continue
|
||||
}
|
||||
assert.Equal(t, want, out.Args[i], "arg[%d]", i)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompileEmpty(t *testing.T) {
|
||||
runCompileCases(t, []compileCase{
|
||||
{subtestName: "empty query yields nil", dslQueryToCompile: "", emptyQueryExpected: true},
|
||||
{subtestName: "whitespace query yields nil", dslQueryToCompile: " ", emptyQueryExpected: true},
|
||||
})
|
||||
}
|
||||
|
||||
func TestCompileName(t *testing.T) {
|
||||
runCompileCases(t, []compileCase{
|
||||
{
|
||||
subtestName: "name equals matches the reserved field or a name label",
|
||||
dslQueryToCompile: "name = 'payment latency'",
|
||||
expectedSQL: `(json_extract("rule"."data", '$.alert') = ? OR COALESCE(json_extract("rule"."data", '$.labels."name"'), '') = ?)`,
|
||||
expectedArgs: []any{"payment latency", "payment latency"},
|
||||
},
|
||||
{
|
||||
subtestName: "name contains escapes wildcards on both sides",
|
||||
dslQueryToCompile: "name CONTAINS '50%'",
|
||||
expectedSQL: `(json_extract("rule"."data", '$.alert') LIKE ? ESCAPE '\' OR COALESCE(json_extract("rule"."data", '$.labels."name"'), '') LIKE ? ESCAPE '\')`,
|
||||
expectedArgs: []any{`%50\%%`, `%50\%%`},
|
||||
},
|
||||
{
|
||||
subtestName: "name ilike",
|
||||
dslQueryToCompile: "name ILIKE 'Prod%'",
|
||||
expectedSQL: `(lower(json_extract("rule"."data", '$.alert')) LIKE LOWER(?) ESCAPE '\' OR lower(COALESCE(json_extract("rule"."data", '$.labels."name"'), '')) LIKE LOWER(?) ESCAPE '\')`,
|
||||
expectedArgs: []any{"Prod%", "Prod%"},
|
||||
},
|
||||
{
|
||||
subtestName: "name in list",
|
||||
dslQueryToCompile: "name IN ['a', 'b']",
|
||||
expectedSQL: `(json_extract("rule"."data", '$.alert') IN (?, ?) OR COALESCE(json_extract("rule"."data", '$.labels."name"'), '') IN (?, ?))`,
|
||||
expectedArgs: []any{"a", "b", "a", "b"},
|
||||
},
|
||||
{
|
||||
subtestName: "name not equals excludes both the reserved field and the label",
|
||||
dslQueryToCompile: "name != 'x'",
|
||||
expectedSQL: `(json_extract("rule"."data", '$.alert') <> ? AND COALESCE(json_extract("rule"."data", '$.labels."name"'), '') <> ?)`,
|
||||
expectedArgs: []any{"x", "x"},
|
||||
},
|
||||
{
|
||||
subtestName: "name exists is label-only since the reserved field disallows it",
|
||||
dslQueryToCompile: "name EXISTS",
|
||||
expectedSQL: `json_extract("rule"."data", '$.labels."name"') IS NOT NULL`,
|
||||
},
|
||||
{
|
||||
subtestName: "range operator rejected on name",
|
||||
dslQueryToCompile: "name > 'x'",
|
||||
expectedErrShouldContain: `operator > is not allowed for key "name"`,
|
||||
},
|
||||
{
|
||||
subtestName: "regexp rejected on name",
|
||||
dslQueryToCompile: "name REGEXP 'x.*'",
|
||||
expectedErrShouldContain: `operator REGEXP is not allowed for key "name"`,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestCompileSeverityAndLabels(t *testing.T) {
|
||||
runCompileCases(t, []compileCase{
|
||||
{
|
||||
subtestName: "severity equals targets labels map",
|
||||
dslQueryToCompile: "severity = 'critical'",
|
||||
expectedSQL: `COALESCE(json_extract("rule"."data", '$.labels."severity"'), '') = ?`,
|
||||
expectedArgs: []any{"critical"},
|
||||
},
|
||||
{
|
||||
subtestName: "severity negation treats a missing label as empty string",
|
||||
dslQueryToCompile: "severity != 'critical'",
|
||||
expectedSQL: `COALESCE(json_extract("rule"."data", '$.labels."severity"'), '') <> ?`,
|
||||
expectedArgs: []any{"critical"},
|
||||
},
|
||||
{
|
||||
subtestName: "severity != empty string excludes rules without severity",
|
||||
dslQueryToCompile: "severity != ''",
|
||||
expectedSQL: `COALESCE(json_extract("rule"."data", '$.labels."severity"'), '') <> ?`,
|
||||
expectedArgs: []any{""},
|
||||
},
|
||||
{
|
||||
subtestName: "severity exists through the alias",
|
||||
dslQueryToCompile: "severity EXISTS",
|
||||
expectedSQL: `json_extract("rule"."data", '$.labels."severity"') IS NOT NULL`,
|
||||
},
|
||||
{
|
||||
subtestName: "severity not exists through the alias",
|
||||
dslQueryToCompile: "severity NOT EXISTS",
|
||||
expectedSQL: `json_extract("rule"."data", '$.labels."severity"') IS NULL`,
|
||||
},
|
||||
{
|
||||
subtestName: "label equals",
|
||||
dslQueryToCompile: "labels.team = 'infra'",
|
||||
expectedSQL: `COALESCE(json_extract("rule"."data", '$.labels."team"'), '') = ?`,
|
||||
expectedArgs: []any{"infra"},
|
||||
},
|
||||
{
|
||||
subtestName: "dotted label key is one map entry",
|
||||
dslQueryToCompile: "labels.k8s.cluster = 'prod-1'",
|
||||
expectedSQL: `COALESCE(json_extract("rule"."data", '$.labels."k8s.cluster"'), '') = ?`,
|
||||
expectedArgs: []any{"prod-1"},
|
||||
},
|
||||
{
|
||||
subtestName: "label key keeps its case",
|
||||
dslQueryToCompile: "labels.Team = 'infra'",
|
||||
expectedSQL: `COALESCE(json_extract("rule"."data", '$.labels."Team"'), '') = ?`,
|
||||
expectedArgs: []any{"infra"},
|
||||
},
|
||||
{
|
||||
subtestName: "label exists",
|
||||
dslQueryToCompile: "labels.team EXISTS",
|
||||
expectedSQL: `json_extract("rule"."data", '$.labels."team"') IS NOT NULL`,
|
||||
},
|
||||
{
|
||||
subtestName: "label not exists",
|
||||
dslQueryToCompile: "labels.team NOT EXISTS",
|
||||
expectedSQL: `json_extract("rule"."data", '$.labels."team"') IS NULL`,
|
||||
},
|
||||
{
|
||||
subtestName: "label not contains includes label-less rules",
|
||||
dslQueryToCompile: "labels.team NOT CONTAINS 'infra'",
|
||||
expectedSQL: `COALESCE(json_extract("rule"."data", '$.labels."team"'), '') NOT LIKE ? ESCAPE '\'`,
|
||||
expectedArgs: []any{"%infra%"},
|
||||
},
|
||||
{
|
||||
subtestName: "label not in includes label-less rules",
|
||||
dslQueryToCompile: "labels.team NOT IN ['a', 'b']",
|
||||
expectedSQL: `COALESCE(json_extract("rule"."data", '$.labels."team"'), '') NOT IN (?, ?)`,
|
||||
expectedArgs: []any{"a", "b"},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestCompileEnums(t *testing.T) {
|
||||
runCompileCases(t, []compileCase{
|
||||
{
|
||||
subtestName: "alert_type equals matches the enum field or an alert_type label",
|
||||
dslQueryToCompile: "alert_type = 'LOGS_BASED_ALERT'",
|
||||
expectedSQL: `(json_extract("rule"."data", '$.alertType') = ? OR COALESCE(json_extract("rule"."data", '$.labels."alert_type"'), '') = ?)`,
|
||||
expectedArgs: []any{"LOGS_BASED_ALERT", "LOGS_BASED_ALERT"},
|
||||
},
|
||||
{
|
||||
subtestName: "rule_type in list",
|
||||
dslQueryToCompile: "rule_type IN ['threshold_rule', 'promql_rule']",
|
||||
expectedSQL: `(json_extract("rule"."data", '$.ruleType') IN (?, ?) OR COALESCE(json_extract("rule"."data", '$.labels."rule_type"'), '') IN (?, ?))`,
|
||||
expectedArgs: []any{"threshold_rule", "promql_rule", "threshold_rule", "promql_rule"},
|
||||
},
|
||||
{
|
||||
subtestName: "invalid alert_type value rejected",
|
||||
dslQueryToCompile: "alert_type = 'bogus'",
|
||||
expectedErrShouldContain: `invalid value "bogus" for "alert_type"`,
|
||||
},
|
||||
{
|
||||
subtestName: "contains on rule_type is label-only since the enum disallows it",
|
||||
dslQueryToCompile: "rule_type CONTAINS 'thresh'",
|
||||
expectedSQL: `COALESCE(json_extract("rule"."data", '$.labels."rule_type"'), '') LIKE ? ESCAPE '\'`,
|
||||
expectedArgs: []any{"%thresh%"},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestCompileAuditColumns(t *testing.T) {
|
||||
createdAt, err := time.Parse(time.RFC3339, "2026-01-02T15:04:05Z")
|
||||
require.NoError(t, err)
|
||||
updatedFrom, err := time.Parse(time.RFC3339, "2026-02-01T00:00:00Z")
|
||||
require.NoError(t, err)
|
||||
updatedTo, err := time.Parse(time.RFC3339, "2026-03-01T00:00:00Z")
|
||||
require.NoError(t, err)
|
||||
|
||||
runCompileCases(t, []compileCase{
|
||||
{
|
||||
subtestName: "created_by equals matches the column or a created_by label",
|
||||
dslQueryToCompile: "created_by = 'nikhil@signoz.io'",
|
||||
expectedSQL: `(rule.created_by = ? OR COALESCE(json_extract("rule"."data", '$.labels."created_by"'), '') = ?)`,
|
||||
expectedArgs: []any{"nikhil@signoz.io", "nikhil@signoz.io"},
|
||||
},
|
||||
{
|
||||
subtestName: "created_at range",
|
||||
dslQueryToCompile: "created_at >= '2026-01-02T15:04:05Z'",
|
||||
expectedSQL: `rule.created_at >= ?`,
|
||||
expectedArgs: []any{createdAt},
|
||||
},
|
||||
{
|
||||
subtestName: "updated_at between",
|
||||
dslQueryToCompile: "updated_at BETWEEN '2026-02-01T00:00:00Z' AND '2026-03-01T00:00:00Z'",
|
||||
expectedSQL: `rule.updated_at BETWEEN ? AND ?`,
|
||||
expectedArgs: []any{updatedFrom, updatedTo},
|
||||
},
|
||||
{
|
||||
subtestName: "non-timestamp rejected on created_at",
|
||||
dslQueryToCompile: "created_at >= 'yesterday'",
|
||||
expectedErrShouldContain: "invalid RFC3339 timestamp",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestCompileFreeText(t *testing.T) {
|
||||
runCompileCases(t, []compileCase{
|
||||
{
|
||||
subtestName: "bare word searches name, description and labels",
|
||||
dslQueryToCompile: "payment",
|
||||
expectedSQL: `(lower(COALESCE(json_extract("rule"."data", '$.alert'), '')) LIKE LOWER(?) ESCAPE '\' ` +
|
||||
`OR lower(COALESCE(json_extract("rule"."data", '$.description'), '')) LIKE LOWER(?) ESCAPE '\' ` +
|
||||
`OR lower(COALESCE(json_extract("rule"."data", '$.labels'), '')) LIKE LOWER(?) ESCAPE '\')`,
|
||||
expectedArgs: []any{"%payment%", "%payment%", "%payment%"},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestCompileComposition(t *testing.T) {
|
||||
runCompileCases(t, []compileCase{
|
||||
{
|
||||
subtestName: "and of label and column",
|
||||
dslQueryToCompile: "labels.team = 'infra' AND created_by = 'x'",
|
||||
expectedSQL: `(COALESCE(json_extract("rule"."data", '$.labels."team"'), '') = ? ` +
|
||||
`AND (rule.created_by = ? OR COALESCE(json_extract("rule"."data", '$.labels."created_by"'), '') = ?))`,
|
||||
expectedArgs: []any{"infra", "x", "x"},
|
||||
},
|
||||
{
|
||||
subtestName: "not wraps the inner predicate",
|
||||
dslQueryToCompile: "NOT (name = 'x')",
|
||||
expectedSQL: `NOT ((json_extract("rule"."data", '$.alert') = ? OR COALESCE(json_extract("rule"."data", '$.labels."name"'), '') = ?))`,
|
||||
expectedArgs: []any{"x", "x"},
|
||||
},
|
||||
{
|
||||
subtestName: "or of name and severity",
|
||||
dslQueryToCompile: "name CONTAINS 'pay' OR severity = 'critical'",
|
||||
expectedSQL: `((json_extract("rule"."data", '$.alert') LIKE ? ESCAPE '\' OR COALESCE(json_extract("rule"."data", '$.labels."name"'), '') LIKE ? ESCAPE '\') ` +
|
||||
`OR COALESCE(json_extract("rule"."data", '$.labels."severity"'), '') = ?)`,
|
||||
expectedArgs: []any{"%pay%", "%pay%", "critical"},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestCompileComplexExamples(t *testing.T) {
|
||||
runCompileCases(t, []compileCase{
|
||||
{
|
||||
subtestName: "name CONTAINS + label = + severity IN + created_by !=",
|
||||
dslQueryToCompile: `name CONTAINS 'latency' AND labels.team = 'payments' ` +
|
||||
`AND severity IN ['critical', 'error'] AND created_by != 'ops@signoz.io'`,
|
||||
expectedSQL: `((json_extract("rule"."data", '$.alert') LIKE ? ESCAPE '\' OR COALESCE(json_extract("rule"."data", '$.labels."name"'), '') LIKE ? ESCAPE '\') ` +
|
||||
`AND COALESCE(json_extract("rule"."data", '$.labels."team"'), '') = ? ` +
|
||||
`AND COALESCE(json_extract("rule"."data", '$.labels."severity"'), '') IN (?, ?) ` +
|
||||
`AND (rule.created_by <> ? AND COALESCE(json_extract("rule"."data", '$.labels."created_by"'), '') <> ?))`,
|
||||
expectedArgs: []any{"%latency%", "%latency%", "payments", "critical", "error", "ops@signoz.io", "ops@signoz.io"},
|
||||
},
|
||||
{
|
||||
subtestName: "nested OR / AND with parens",
|
||||
dslQueryToCompile: `(labels.env IN ['prod', 'staging'] OR name LIKE '%prod%') ` +
|
||||
`AND (severity = 'critical' OR labels.team EXISTS)`,
|
||||
expectedSQL: `((COALESCE(json_extract("rule"."data", '$.labels."env"'), '') IN (?, ?) ` +
|
||||
`OR (json_extract("rule"."data", '$.alert') LIKE ? ESCAPE '\' OR COALESCE(json_extract("rule"."data", '$.labels."name"'), '') LIKE ? ESCAPE '\')) ` +
|
||||
`AND (COALESCE(json_extract("rule"."data", '$.labels."severity"'), '') = ? ` +
|
||||
`OR json_extract("rule"."data", '$.labels."team"') IS NOT NULL))`,
|
||||
expectedArgs: []any{"prod", "staging", "%prod%", "%prod%", "critical"},
|
||||
},
|
||||
{
|
||||
subtestName: "NOT over a group ANDed with an enum",
|
||||
dslQueryToCompile: `NOT (labels.team = 'infra' OR name CONTAINS 'cpu') AND alert_type = 'METRIC_BASED_ALERT'`,
|
||||
expectedSQL: `(NOT ((COALESCE(json_extract("rule"."data", '$.labels."team"'), '') = ? ` +
|
||||
`OR (json_extract("rule"."data", '$.alert') LIKE ? ESCAPE '\' OR COALESCE(json_extract("rule"."data", '$.labels."name"'), '') LIKE ? ESCAPE '\'))) ` +
|
||||
`AND (json_extract("rule"."data", '$.alertType') = ? OR COALESCE(json_extract("rule"."data", '$.labels."alert_type"'), '') = ?))`,
|
||||
expectedArgs: []any{"infra", "%cpu%", "%cpu%", "METRIC_BASED_ALERT", "METRIC_BASED_ALERT"},
|
||||
},
|
||||
{
|
||||
subtestName: "free text with three-level nesting and a timestamp",
|
||||
dslQueryToCompile: `prod AND (name ILIKE '%pay%' ` +
|
||||
`OR (labels.team != 'infra' AND updated_at > '2026-01-02T15:04:05Z'))`,
|
||||
expectedSQL: `((lower(COALESCE(json_extract("rule"."data", '$.alert'), '')) LIKE LOWER(?) ESCAPE '\' ` +
|
||||
`OR lower(COALESCE(json_extract("rule"."data", '$.description'), '')) LIKE LOWER(?) ESCAPE '\' ` +
|
||||
`OR lower(COALESCE(json_extract("rule"."data", '$.labels'), '')) LIKE LOWER(?) ESCAPE '\') ` +
|
||||
`AND ((lower(json_extract("rule"."data", '$.alert')) LIKE LOWER(?) ESCAPE '\' ` +
|
||||
`OR lower(COALESCE(json_extract("rule"."data", '$.labels."name"'), '')) LIKE LOWER(?) ESCAPE '\') ` +
|
||||
`OR (COALESCE(json_extract("rule"."data", '$.labels."team"'), '') <> ? AND rule.updated_at > ?)))`,
|
||||
expectedArgs: []any{"%prod%", "%prod%", "%prod%", "%pay%", "%pay%", "infra",
|
||||
time.Date(2026, 1, 2, 15, 4, 5, 0, time.UTC)},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestCompileBareLabelKeys(t *testing.T) {
|
||||
runCompileCases(t, []compileCase{
|
||||
{
|
||||
subtestName: "bare key compiles to a label match",
|
||||
dslQueryToCompile: "team = 'infra'",
|
||||
expectedSQL: `COALESCE(json_extract("rule"."data", '$.labels."team"'), '') = ?`,
|
||||
expectedArgs: []any{"infra"},
|
||||
},
|
||||
{
|
||||
subtestName: "bare key keeps its case",
|
||||
dslQueryToCompile: "Team CONTAINS 'inf'",
|
||||
expectedSQL: `COALESCE(json_extract("rule"."data", '$.labels."Team"'), '') LIKE ? ESCAPE '\'`,
|
||||
expectedArgs: []any{"%inf%"},
|
||||
},
|
||||
{
|
||||
subtestName: "bare key exists",
|
||||
dslQueryToCompile: "env EXISTS",
|
||||
expectedSQL: `json_extract("rule"."data", '$.labels."env"') IS NOT NULL`,
|
||||
},
|
||||
{
|
||||
subtestName: "state compiles as a label lookup not a rule state filter",
|
||||
dslQueryToCompile: "state = 'firing'",
|
||||
expectedSQL: `COALESCE(json_extract("rule"."data", '$.labels."state"'), '') = ?`,
|
||||
expectedArgs: []any{"firing"},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestCompileReservedLabelCollisions(t *testing.T) {
|
||||
runCompileCases(t, []compileCase{
|
||||
{
|
||||
subtestName: "uppercase reserved key matches the reserved field or the exact-case label",
|
||||
dslQueryToCompile: "NAME = 'x'",
|
||||
expectedSQL: `(json_extract("rule"."data", '$.alert') = ? OR COALESCE(json_extract("rule"."data", '$.labels."NAME"'), '') = ?)`,
|
||||
expectedArgs: []any{"x", "x"},
|
||||
},
|
||||
{
|
||||
subtestName: "severity spelled exactly stays a single predicate",
|
||||
dslQueryToCompile: "severity = 'critical'",
|
||||
expectedSQL: `COALESCE(json_extract("rule"."data", '$.labels."severity"'), '') = ?`,
|
||||
expectedArgs: []any{"critical"},
|
||||
},
|
||||
{
|
||||
subtestName: "differently cased severity matches both label spellings",
|
||||
dslQueryToCompile: "Severity = 'critical'",
|
||||
expectedSQL: `(COALESCE(json_extract("rule"."data", '$.labels."severity"'), '') = ? ` +
|
||||
`OR COALESCE(json_extract("rule"."data", '$.labels."Severity"'), '') = ?)`,
|
||||
expectedArgs: []any{"critical", "critical"},
|
||||
},
|
||||
{
|
||||
subtestName: "range operator stays reserved-only since labels disallow it",
|
||||
dslQueryToCompile: "created_at >= '2026-01-02T15:04:05Z'",
|
||||
expectedSQL: `rule.created_at >= ?`,
|
||||
expectedArgs: []any{time.Date(2026, 1, 2, 15, 4, 5, 0, time.UTC)},
|
||||
},
|
||||
{
|
||||
subtestName: "labels prefix targets only the label on a collision",
|
||||
dslQueryToCompile: "labels.name = 'x'",
|
||||
expectedSQL: `COALESCE(json_extract("rule"."data", '$.labels."name"'), '') = ?`,
|
||||
expectedArgs: []any{"x"},
|
||||
},
|
||||
{
|
||||
subtestName: "not in excludes both the reserved field and the label",
|
||||
dslQueryToCompile: "created_by NOT IN ['a', 'b']",
|
||||
expectedSQL: `(rule.created_by NOT IN (?, ?) ` +
|
||||
`AND COALESCE(json_extract("rule"."data", '$.labels."created_by"'), '') NOT IN (?, ?))`,
|
||||
expectedArgs: []any{"a", "b", "a", "b"},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestCompileErrors(t *testing.T) {
|
||||
runCompileCases(t, []compileCase{
|
||||
{
|
||||
subtestName: "range operator rejected on a bare label key",
|
||||
dslQueryToCompile: "team > 'infra'",
|
||||
expectedErrShouldContain: `operator > is not allowed on the label filter "team"`,
|
||||
},
|
||||
{
|
||||
subtestName: "syntax error surfaces position",
|
||||
dslQueryToCompile: "created_by ==== (((",
|
||||
expectedErrShouldContain: "syntax error",
|
||||
},
|
||||
{
|
||||
subtestName: "like pattern with dangling escape rejected",
|
||||
dslQueryToCompile: `name LIKE 'prod\\'`,
|
||||
expectedErrShouldContain: "must not end with an unescaped backslash",
|
||||
},
|
||||
{
|
||||
subtestName: "ilike pattern with dangling escape rejected",
|
||||
dslQueryToCompile: `name ILIKE '%\\'`,
|
||||
expectedErrShouldContain: "must not end with an unescaped backslash",
|
||||
},
|
||||
{
|
||||
subtestName: "label like pattern with dangling escape rejected",
|
||||
dslQueryToCompile: `labels.team NOT LIKE 'infra\\'`,
|
||||
expectedErrShouldContain: "must not end with an unescaped backslash",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestCompileTrailingLiteralBackslash(t *testing.T) {
|
||||
runCompileCases(t, []compileCase{
|
||||
{
|
||||
subtestName: "escaped trailing backslash compiles",
|
||||
dslQueryToCompile: `name LIKE '%\\\\'`,
|
||||
expectedSQL: `(json_extract("rule"."data", '$.alert') LIKE ? ESCAPE '\' OR COALESCE(json_extract("rule"."data", '$.labels."name"'), '') LIKE ? ESCAPE '\')`,
|
||||
expectedArgs: []any{`%\\`, `%\\`},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Guards that every ruletypes.ReservedOps key has a case in resolveReservedKey.
|
||||
func TestCompileReservedKeysAllHandled(t *testing.T) {
|
||||
sampleQueries := map[ruletypes.DSLKey]string{
|
||||
ruletypes.DSLKeyName: "name = 'x'",
|
||||
ruletypes.DSLKeySeverity: "severity = 'critical'",
|
||||
ruletypes.DSLKeyCreatedBy: "created_by = 'x'",
|
||||
ruletypes.DSLKeyUpdatedBy: "updated_by = 'x'",
|
||||
ruletypes.DSLKeyCreatedAt: "created_at >= '2026-01-02T15:04:05Z'",
|
||||
ruletypes.DSLKeyUpdatedAt: "updated_at >= '2026-01-02T15:04:05Z'",
|
||||
ruletypes.DSLKeyAlertType: "alert_type = 'METRIC_BASED_ALERT'",
|
||||
ruletypes.DSLKeyRuleType: "rule_type = 'threshold_rule'",
|
||||
}
|
||||
|
||||
for key := range ruletypes.ReservedOps {
|
||||
query, ok := sampleQueries[key]
|
||||
require.True(t, ok, "no sample query for reserved key %q, add one", key)
|
||||
|
||||
out, err := Compile(query, formatter(t))
|
||||
require.NoError(t, err, "reserved key %q failed to compile", key)
|
||||
assert.False(t, out.IsEmpty(), "reserved key %q compiled to empty SQL", key)
|
||||
}
|
||||
}
|
||||
|
||||
func formatter(t *testing.T) sqlstore.SQLFormatter {
|
||||
t.Helper()
|
||||
p := sqlstoretest.New(sqlstore.Config{Provider: "sqlite"}, sqlmock.QueryMatcherEqual)
|
||||
return p.Formatter()
|
||||
}
|
||||
|
||||
func normalizeSQL(s string) string {
|
||||
s = strings.Join(strings.Fields(s), " ")
|
||||
s = strings.ReplaceAll(s, "( ", "(")
|
||||
s = strings.ReplaceAll(s, " )", ")")
|
||||
return s
|
||||
}
|
||||
@@ -3,7 +3,6 @@ package sqlrulestore
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"slices"
|
||||
|
||||
@@ -90,46 +89,6 @@ func (r *rule) DeleteRule(ctx context.Context, orgID valuer.UUID, id valuer.UUID
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *rule) GetStoredRulesMatching(ctx context.Context, orgID string, query string) ([]*ruletypes.StorableRule, error) {
|
||||
compiled, err := Compile(query, r.sqlstore.Formatter())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rules := make([]*ruletypes.StorableRule, 0)
|
||||
q := r.sqlstore.
|
||||
BunDB().
|
||||
NewSelect().
|
||||
Model(&rules).
|
||||
Where("org_id = ?", orgID)
|
||||
if !compiled.IsEmpty() {
|
||||
q = q.Where(compiled.SQL, compiled.Args...)
|
||||
}
|
||||
if err := q.Scan(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return rules, nil
|
||||
}
|
||||
|
||||
func (r *rule) GetStoredRuleLabels(ctx context.Context, orgID string) ([]string, error) {
|
||||
labelsExpression := string(r.sqlstore.Formatter().JSONExtractString("rule.data", "$.labels"))
|
||||
|
||||
labels := make([]string, 0)
|
||||
err := r.sqlstore.
|
||||
BunDB().
|
||||
NewSelect().
|
||||
Model((*ruletypes.StorableRule)(nil)).
|
||||
ColumnExpr(fmt.Sprintf("COALESCE(%s, '')", labelsExpression)).
|
||||
Where("org_id = ?", orgID).
|
||||
Scan(ctx, &labels)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return labels, nil
|
||||
}
|
||||
|
||||
func (r *rule) GetStoredRules(ctx context.Context, orgID string) ([]*ruletypes.StorableRule, error) {
|
||||
rules := make([]*ruletypes.StorableRule, 0)
|
||||
err := r.sqlstore.
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
package sqlrulestore
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
ruletypes "github.com/SigNoz/signoz/pkg/types/ruletypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
func (r *rule) CreateRuleView(ctx context.Context, view *ruletypes.RuleView) error {
|
||||
_, err := r.sqlstore.
|
||||
BunDBCtx(ctx).
|
||||
NewInsert().
|
||||
Model(view).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return r.sqlstore.WrapAlreadyExistsErrf(err, errors.CodeAlreadyExists, "rule view with id %s already exists", view.ID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *rule) GetRuleView(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*ruletypes.RuleView, error) {
|
||||
view := new(ruletypes.RuleView)
|
||||
err := r.sqlstore.
|
||||
BunDB().
|
||||
NewSelect().
|
||||
Model(view).
|
||||
Where("id = ?", id).
|
||||
Where("org_id = ?", orgID).
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, r.sqlstore.WrapNotFoundErrf(err, ruletypes.ErrCodeRuleViewNotFound, "rule view with id %s doesn't exist", id)
|
||||
}
|
||||
return view, nil
|
||||
}
|
||||
|
||||
func (r *rule) ListRuleViews(ctx context.Context, orgID valuer.UUID) ([]*ruletypes.RuleView, error) {
|
||||
views := make([]*ruletypes.RuleView, 0)
|
||||
err := r.sqlstore.
|
||||
BunDB().
|
||||
NewSelect().
|
||||
Model(&views).
|
||||
Where("org_id = ?", orgID).
|
||||
OrderExpr("updated_at DESC").
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.WrapInternalf(err, errors.CodeInternal, "couldn't list rule views")
|
||||
}
|
||||
return views, nil
|
||||
}
|
||||
|
||||
func (r *rule) UpdateRuleView(ctx context.Context, view *ruletypes.RuleView) error {
|
||||
res, err := r.sqlstore.
|
||||
BunDBCtx(ctx).
|
||||
NewUpdate().
|
||||
Model(view).
|
||||
WherePK().
|
||||
Where("org_id = ?", view.OrgID).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return errors.WrapInternalf(err, errors.CodeInternal, "couldn't update rule view")
|
||||
}
|
||||
rows, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return errors.WrapInternalf(err, errors.CodeInternal, "couldn't read rule view update result")
|
||||
}
|
||||
if rows == 0 {
|
||||
return errors.Newf(errors.TypeNotFound, ruletypes.ErrCodeRuleViewNotFound, "rule view with id %s doesn't exist", view.ID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *rule) DeleteRuleView(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error {
|
||||
res, err := r.sqlstore.
|
||||
BunDBCtx(ctx).
|
||||
NewDelete().
|
||||
Model(new(ruletypes.RuleView)).
|
||||
Where("id = ?", id).
|
||||
Where("org_id = ?", orgID).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return errors.WrapInternalf(err, errors.CodeInternal, "couldn't delete rule view")
|
||||
}
|
||||
rows, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return errors.WrapInternalf(err, errors.CodeInternal, "couldn't read rule view delete result")
|
||||
}
|
||||
if rows == 0 {
|
||||
return errors.Newf(errors.TypeNotFound, ruletypes.ErrCodeRuleViewNotFound, "rule view with id %s doesn't exist", id)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -43,29 +43,6 @@ func (handler *handler) ListRules(rw http.ResponseWriter, req *http.Request) {
|
||||
render.Success(rw, http.StatusOK, view)
|
||||
}
|
||||
|
||||
func (handler *handler) ListRulesV3(rw http.ResponseWriter, req *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
params := new(ruletypes.ListRulesParams)
|
||||
if err := binding.Query.BindQuery(req.URL.Query(), params); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
if err := params.Validate(); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
listableRules, err := handler.ruler.ListRules(ctx, params)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusOK, listableRules)
|
||||
}
|
||||
|
||||
func (handler *handler) GetRuleByID(rw http.ResponseWriter, req *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
|
||||
defer cancel()
|
||||
@@ -345,122 +322,3 @@ func (handler *handler) DeleteDowntimeScheduleByID(rw http.ResponseWriter, req *
|
||||
|
||||
render.Success(rw, http.StatusNoContent, nil)
|
||||
}
|
||||
|
||||
func (handler *handler) ListRuleViews(rw http.ResponseWriter, req *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(req.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, err)
|
||||
return
|
||||
}
|
||||
|
||||
views, err := handler.ruler.ListRuleViews(ctx, orgID)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusOK, views)
|
||||
}
|
||||
|
||||
func (handler *handler) CreateRuleView(rw http.ResponseWriter, req *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(req.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, err)
|
||||
return
|
||||
}
|
||||
|
||||
var postable ruletypes.PostableRuleView
|
||||
if err := binding.JSON.BindBody(req.Body, &postable); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
view, err := handler.ruler.CreateRuleView(ctx, orgID, postable)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusCreated, view)
|
||||
}
|
||||
|
||||
func (handler *handler) UpdateRuleView(rw http.ResponseWriter, req *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(req.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, err)
|
||||
return
|
||||
}
|
||||
|
||||
id, err := valuer.NewUUID(mux.Vars(req)["id"])
|
||||
if err != nil {
|
||||
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "id is not a valid uuid-v7"))
|
||||
return
|
||||
}
|
||||
|
||||
var updatable ruletypes.UpdatableRuleView
|
||||
if err := binding.JSON.BindBody(req.Body, &updatable); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
view, err := handler.ruler.UpdateRuleView(ctx, orgID, id, updatable)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusOK, view)
|
||||
}
|
||||
|
||||
func (handler *handler) DeleteRuleView(rw http.ResponseWriter, req *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(req.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, err)
|
||||
return
|
||||
}
|
||||
|
||||
id, err := valuer.NewUUID(mux.Vars(req)["id"])
|
||||
if err != nil {
|
||||
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "id is not a valid uuid-v7"))
|
||||
return
|
||||
}
|
||||
|
||||
if err := handler.ruler.DeleteRuleView(ctx, orgID, id); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusNoContent, nil)
|
||||
}
|
||||
|
||||
@@ -116,10 +116,6 @@ func (provider *provider) ListRuleStates(ctx context.Context) (*ruletypes.Gettab
|
||||
return provider.manager.ListRuleStates(ctx)
|
||||
}
|
||||
|
||||
func (provider *provider) ListRules(ctx context.Context, params *ruletypes.ListRulesParams) (*ruletypes.ListableRules, error) {
|
||||
return provider.manager.ListRules(ctx, params)
|
||||
}
|
||||
|
||||
func (provider *provider) GetRule(ctx context.Context, id valuer.UUID) (*ruletypes.GettableRule, error) {
|
||||
return provider.manager.GetRule(ctx, id)
|
||||
}
|
||||
@@ -147,41 +143,3 @@ func (provider *provider) TestNotification(ctx context.Context, orgID valuer.UUI
|
||||
func (provider *provider) MaintenanceStore() alertmanagertypes.MaintenanceStore {
|
||||
return provider.manager.MaintenanceStore()
|
||||
}
|
||||
|
||||
func (provider *provider) CreateRuleView(ctx context.Context, orgID valuer.UUID, postable ruletypes.PostableRuleView) (*ruletypes.RuleView, error) {
|
||||
if err := postable.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
view := postable.NewRuleView(orgID)
|
||||
if err := provider.ruleStore.CreateRuleView(ctx, view); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return view, nil
|
||||
}
|
||||
|
||||
func (provider *provider) ListRuleViews(ctx context.Context, orgID valuer.UUID) (*ruletypes.ListableRuleViews, error) {
|
||||
views, err := provider.ruleStore.ListRuleViews(ctx, orgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &ruletypes.ListableRuleViews{Views: views}, nil
|
||||
}
|
||||
|
||||
func (provider *provider) UpdateRuleView(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatable ruletypes.UpdatableRuleView) (*ruletypes.RuleView, error) {
|
||||
if err := updatable.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
view, err := provider.ruleStore.GetRuleView(ctx, orgID, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
view.Update(updatable)
|
||||
if err := provider.ruleStore.UpdateRuleView(ctx, view); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return view, nil
|
||||
}
|
||||
|
||||
func (provider *provider) DeleteRuleView(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error {
|
||||
return provider.ruleStore.DeleteRuleView(ctx, orgID, id)
|
||||
}
|
||||
|
||||
@@ -255,7 +255,6 @@ func NewSQLMigrationProviderFactories(
|
||||
sqlmigration.NewNormalizeQuickFilterFieldsFactory(sqlstore),
|
||||
sqlmigration.NewAddSpanMapperOriginFactory(sqlstore, sqlschema),
|
||||
sqlmigration.NewAddCloudIntegrationTuplesFactory(sqlstore),
|
||||
sqlmigration.NewAddRuleViewFactory(sqlstore, sqlschema),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
package sqlmigration
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/sqlschema"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/migrate"
|
||||
)
|
||||
|
||||
type addRuleView struct {
|
||||
sqlstore sqlstore.SQLStore
|
||||
sqlschema sqlschema.SQLSchema
|
||||
}
|
||||
|
||||
func NewAddRuleViewFactory(sqlstore sqlstore.SQLStore, sqlschema sqlschema.SQLSchema) factory.ProviderFactory[SQLMigration, Config] {
|
||||
return factory.NewProviderFactory(factory.MustNewName("add_rule_view"), func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
|
||||
return &addRuleView{
|
||||
sqlstore: sqlstore,
|
||||
sqlschema: sqlschema,
|
||||
}, nil
|
||||
})
|
||||
}
|
||||
|
||||
func (migration *addRuleView) Register(migrations *migrate.Migrations) error {
|
||||
return migrations.Register(migration.Up, migration.Down)
|
||||
}
|
||||
|
||||
func (migration *addRuleView) Up(ctx context.Context, db *bun.DB) error {
|
||||
tx, err := db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = tx.Rollback() }()
|
||||
|
||||
sqls := migration.sqlschema.Operator().CreateTable(&sqlschema.Table{
|
||||
Name: "rule_view",
|
||||
Columns: []*sqlschema.Column{
|
||||
{Name: "id", DataType: sqlschema.DataTypeText, Nullable: false},
|
||||
{Name: "name", DataType: sqlschema.DataTypeText, Nullable: false},
|
||||
{Name: "data", DataType: sqlschema.DataTypeText, Nullable: false},
|
||||
{Name: "org_id", DataType: sqlschema.DataTypeText, Nullable: false},
|
||||
{Name: "created_at", DataType: sqlschema.DataTypeTimestamp, Nullable: false},
|
||||
{Name: "updated_at", DataType: sqlschema.DataTypeTimestamp, Nullable: false},
|
||||
},
|
||||
PrimaryKeyConstraint: &sqlschema.PrimaryKeyConstraint{ColumnNames: []sqlschema.ColumnName{"id"}},
|
||||
ForeignKeyConstraints: []*sqlschema.ForeignKeyConstraint{
|
||||
{
|
||||
ReferencingColumnName: sqlschema.ColumnName("org_id"),
|
||||
ReferencedTableName: sqlschema.TableName("organizations"),
|
||||
ReferencedColumnName: sqlschema.ColumnName("id"),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
for _, sql := range sqls {
|
||||
if _, err := tx.ExecContext(ctx, string(sql)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := tx.NewCreateIndex().
|
||||
Table("rule_view").
|
||||
Column("org_id").
|
||||
Index("idx_rule_view_org_id").
|
||||
IfNotExists().
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (migration *addRuleView) Down(_ context.Context, _ *bun.DB) error {
|
||||
return nil
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package sqlitesqlstore
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
@@ -26,12 +25,6 @@ func (f *formatter) JSONExtractString(column, path string) []byte {
|
||||
return sql
|
||||
}
|
||||
|
||||
func (f *formatter) JSONExtractMapValue(column, mapField, key string) []byte {
|
||||
// Quote the key as one path segment; a double quote in it is inexpressible in sqlite JSON paths.
|
||||
escapedKey := strings.NewReplacer(`\`, `\\`).Replace(key)
|
||||
return f.JSONExtractString(column, fmt.Sprintf(`$.%s."%s"`, mapField, escapedKey))
|
||||
}
|
||||
|
||||
func (f *formatter) JSONType(column, path string) []byte {
|
||||
var sql []byte
|
||||
sql = append(sql, "json_type("...)
|
||||
|
||||
@@ -55,60 +55,6 @@ func TestJSONExtractString(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSONExtractMapValue(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
column string
|
||||
mapField string
|
||||
key string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "plain key",
|
||||
column: "data",
|
||||
mapField: "labels",
|
||||
key: "team",
|
||||
expected: `json_extract("data", '$.labels."team"')`,
|
||||
},
|
||||
{
|
||||
name: "dotted key stays one map entry",
|
||||
column: "data",
|
||||
mapField: "labels",
|
||||
key: "k8s.cluster",
|
||||
expected: `json_extract("data", '$.labels."k8s.cluster"')`,
|
||||
},
|
||||
{
|
||||
name: "backslash in key is escaped",
|
||||
column: "data",
|
||||
mapField: "labels",
|
||||
key: `a\b`,
|
||||
expected: `json_extract("data", '$.labels."a\\b"')`,
|
||||
},
|
||||
{
|
||||
name: "single quote in key is doubled",
|
||||
column: "data",
|
||||
mapField: "labels",
|
||||
key: "o'brien",
|
||||
expected: `json_extract("data", '$.labels."o''brien"')`,
|
||||
},
|
||||
{
|
||||
name: "qualified column",
|
||||
column: "rule.data",
|
||||
mapField: "labels",
|
||||
key: "severity",
|
||||
expected: `json_extract("rule"."data", '$.labels."severity"')`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
f := newFormatter(sqlitedialect.New())
|
||||
got := string(f.JSONExtractMapValue(tt.column, tt.mapField, tt.key))
|
||||
assert.Equal(t, tt.expected, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSONType(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
@@ -114,9 +114,6 @@ type SQLFormatter interface {
|
||||
// JSONKeys return extracted key from json as well as alias to be used for select and where clause
|
||||
JSONKeys(column, path, alias string) ([]byte, []byte)
|
||||
|
||||
// JSONExtractMapValue extracts one key's value from a JSON object field; dots in the key are not path nesting.
|
||||
JSONExtractMapValue(column, mapField, key string) []byte
|
||||
|
||||
// TextToJsonColumn converts a text column to JSON type
|
||||
TextToJsonColumn(column string) []byte
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package sqlstoretest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
@@ -26,11 +25,6 @@ func (f *formatter) JSONExtractString(column, path string) []byte {
|
||||
return sql
|
||||
}
|
||||
|
||||
func (f *formatter) JSONExtractMapValue(column, mapField, key string) []byte {
|
||||
escapedKey := strings.NewReplacer(`\`, `\\`).Replace(key)
|
||||
return f.JSONExtractString(column, fmt.Sprintf(`$.%s."%s"`, mapField, escapedKey))
|
||||
}
|
||||
|
||||
func (f *formatter) JSONType(column, path string) []byte {
|
||||
var sql []byte
|
||||
sql = append(sql, "json_type("...)
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
package sqlstoretest
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/uptrace/bun/dialect/sqlitedialect"
|
||||
)
|
||||
|
||||
func TestJSONExtractMapValue(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
column string
|
||||
mapField string
|
||||
key string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "plain key",
|
||||
column: "data",
|
||||
mapField: "labels",
|
||||
key: "team",
|
||||
expected: `json_extract("data", '$.labels."team"')`,
|
||||
},
|
||||
{
|
||||
name: "dotted key stays one map entry",
|
||||
column: "data",
|
||||
mapField: "labels",
|
||||
key: "k8s.cluster",
|
||||
expected: `json_extract("data", '$.labels."k8s.cluster"')`,
|
||||
},
|
||||
{
|
||||
name: "backslash in key is escaped",
|
||||
column: "data",
|
||||
mapField: "labels",
|
||||
key: `a\b`,
|
||||
expected: `json_extract("data", '$.labels."a\\b"')`,
|
||||
},
|
||||
{
|
||||
name: "single quote in key is doubled",
|
||||
column: "data",
|
||||
mapField: "labels",
|
||||
key: "o'brien",
|
||||
expected: `json_extract("data", '$.labels."o''brien"')`,
|
||||
},
|
||||
{
|
||||
name: "qualified column",
|
||||
column: "rule.data",
|
||||
mapField: "labels",
|
||||
key: "severity",
|
||||
expected: `json_extract("rule"."data", '$.labels."severity"')`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
f := newFormatter(sqlitedialect.New())
|
||||
got := string(f.JSONExtractMapValue(tt.column, tt.mapField, tt.key))
|
||||
assert.Equal(t, tt.expected, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,8 @@
|
||||
package dashboardtypes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
@@ -22,7 +16,6 @@ var (
|
||||
ErrCodeDashboardImmutable = errors.MustNewCode("dashboard_immutable")
|
||||
ErrCodeDashboardInvalidPatch = errors.MustNewCode("dashboard_invalid_patch")
|
||||
ErrCodeDashboardMigrationFailed = errors.MustNewCode("dashboard_migration_failed")
|
||||
ErrCodeDashboardV1Deprecated = errors.MustNewCode("dashboard_deprecated")
|
||||
)
|
||||
|
||||
type StorableDashboard struct {
|
||||
@@ -38,31 +31,8 @@ type StorableDashboard struct {
|
||||
Name string `bun:"name,type:text,notnull"`
|
||||
}
|
||||
|
||||
type Dashboard struct {
|
||||
types.TimeAuditable
|
||||
types.UserAuditable
|
||||
|
||||
ID string `json:"id"`
|
||||
Data StorableDashboardData `json:"data"`
|
||||
Locked bool `json:"locked"`
|
||||
OrgID valuer.UUID `json:"org_id"`
|
||||
Source Source `json:"source"`
|
||||
}
|
||||
|
||||
type LockUnlockDashboard struct {
|
||||
Locked *bool `json:"locked"`
|
||||
}
|
||||
|
||||
type (
|
||||
StorableDashboardData map[string]interface{}
|
||||
|
||||
GettableDashboard = Dashboard
|
||||
|
||||
UpdatableDashboard = StorableDashboardData
|
||||
|
||||
PostableDashboard = StorableDashboardData
|
||||
|
||||
ListableDashboard []*GettableDashboard
|
||||
StorableDashboardData map[string]any
|
||||
)
|
||||
|
||||
// readString reads a string field from the untyped data blob, yielding "" when
|
||||
@@ -72,135 +42,6 @@ func (d StorableDashboardData) readString(key string) string {
|
||||
return s
|
||||
}
|
||||
|
||||
func NewStorableDashboardFromDashboard(dashboard *Dashboard) (*StorableDashboard, error) {
|
||||
dashboardID, err := valuer.NewUUID(dashboard.ID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "id is not a valid uuid")
|
||||
}
|
||||
|
||||
if !dashboard.Source.IsValid() {
|
||||
return nil, errors.Newf(errors.TypeInvalidInput, ErrCodeDashboardInvalidSource, "invalid dashboard source %q, must be one of user, system, integration", dashboard.Source.StringValue())
|
||||
}
|
||||
|
||||
return &StorableDashboard{
|
||||
Identifiable: types.Identifiable{
|
||||
ID: dashboardID,
|
||||
},
|
||||
TimeAuditable: types.TimeAuditable{
|
||||
CreatedAt: dashboard.CreatedAt,
|
||||
UpdatedAt: dashboard.UpdatedAt,
|
||||
},
|
||||
UserAuditable: types.UserAuditable{
|
||||
CreatedBy: dashboard.CreatedBy,
|
||||
UpdatedBy: dashboard.UpdatedBy,
|
||||
},
|
||||
OrgID: dashboard.OrgID,
|
||||
Data: dashboard.Data,
|
||||
Locked: dashboard.Locked,
|
||||
Source: dashboard.Source,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func NewDashboard(orgID valuer.UUID, createdBy string, source Source, storableDashboardData StorableDashboardData) (*Dashboard, error) {
|
||||
if !source.IsValid() {
|
||||
return nil, errors.Newf(errors.TypeInvalidInput, ErrCodeDashboardInvalidSource, "invalid dashboard source %q, must be one of user, system, integration", source.StringValue())
|
||||
}
|
||||
|
||||
currentTime := time.Now()
|
||||
|
||||
return &Dashboard{
|
||||
ID: valuer.GenerateUUID().StringValue(),
|
||||
TimeAuditable: types.TimeAuditable{
|
||||
CreatedAt: currentTime,
|
||||
UpdatedAt: currentTime,
|
||||
},
|
||||
UserAuditable: types.UserAuditable{
|
||||
CreatedBy: createdBy,
|
||||
UpdatedBy: createdBy,
|
||||
},
|
||||
OrgID: orgID,
|
||||
Data: storableDashboardData,
|
||||
Locked: source == SourceIntegration,
|
||||
Source: source,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func NewDashboardFromStorableDashboard(storableDashboard *StorableDashboard) *Dashboard {
|
||||
return &Dashboard{
|
||||
ID: storableDashboard.ID.StringValue(),
|
||||
TimeAuditable: types.TimeAuditable{
|
||||
CreatedAt: storableDashboard.CreatedAt,
|
||||
UpdatedAt: storableDashboard.UpdatedAt,
|
||||
},
|
||||
UserAuditable: types.UserAuditable{
|
||||
CreatedBy: storableDashboard.CreatedBy,
|
||||
UpdatedBy: storableDashboard.UpdatedBy,
|
||||
},
|
||||
OrgID: storableDashboard.OrgID,
|
||||
Data: storableDashboard.Data,
|
||||
Locked: storableDashboard.Locked,
|
||||
Source: storableDashboard.Source,
|
||||
}
|
||||
}
|
||||
|
||||
func NewDashboardsFromStorableDashboards(storableDashboards []*StorableDashboard) []*Dashboard {
|
||||
dashboards := make([]*Dashboard, len(storableDashboards))
|
||||
for idx, storableDashboard := range storableDashboards {
|
||||
dashboards[idx] = NewDashboardFromStorableDashboard(storableDashboard)
|
||||
}
|
||||
|
||||
return dashboards
|
||||
}
|
||||
|
||||
func NewGettableDashboardsFromDashboards(dashboards []*Dashboard) ([]*GettableDashboard, error) {
|
||||
gettableDashboards := make([]*GettableDashboard, len(dashboards))
|
||||
for idx, dashboard := range dashboards {
|
||||
gettableDashboard, err := NewGettableDashboardFromDashboard(dashboard)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
gettableDashboards[idx] = gettableDashboard
|
||||
}
|
||||
return gettableDashboards, nil
|
||||
}
|
||||
|
||||
func NewGettableDashboardFromDashboard(dashboard *Dashboard) (*GettableDashboard, error) {
|
||||
return &GettableDashboard{
|
||||
ID: dashboard.ID,
|
||||
TimeAuditable: dashboard.TimeAuditable,
|
||||
UserAuditable: dashboard.UserAuditable,
|
||||
OrgID: dashboard.OrgID,
|
||||
Data: dashboard.Data,
|
||||
Locked: dashboard.Locked,
|
||||
Source: dashboard.Source,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (storableDashboardData *StorableDashboardData) GetWidgetIds() []string {
|
||||
data := *storableDashboardData
|
||||
widgetIds := []string{}
|
||||
if data != nil && data["widgets"] != nil {
|
||||
widgets, ok := data["widgets"]
|
||||
if ok {
|
||||
data, ok := widgets.([]interface{})
|
||||
if ok {
|
||||
for _, widget := range data {
|
||||
sData, ok := widget.(map[string]interface{})
|
||||
if ok && sData["query"] != nil && sData["id"] != nil {
|
||||
id, ok := sData["id"].(string)
|
||||
|
||||
if ok {
|
||||
widgetIds = append(widgetIds, id)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return widgetIds
|
||||
}
|
||||
|
||||
// ErrIfNotDeletable gates deletion on the columns alone, never on Data, so a
|
||||
// dashboard whose data is corrupt or stuck on the v1 schema stays deletable.
|
||||
func (storable StorableDashboard) ErrIfNotDeletable() error {
|
||||
@@ -212,247 +53,3 @@ func (storable StorableDashboard) ErrIfNotDeletable() error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dashboard *Dashboard) ErrIfNotMutable() error {
|
||||
if dashboard.Source == SourceIntegration {
|
||||
return errors.Newf(errors.TypeInvalidInput, ErrCodeDashboardImmutable, "integration dashboards cannot be modified")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dashboard *Dashboard) ErrIfNotDeletable() error {
|
||||
if err := dashboard.ErrIfNotMutable(); err != nil {
|
||||
return err
|
||||
}
|
||||
if dashboard.Source == SourceSystem {
|
||||
return errors.Newf(errors.TypeInvalidInput, ErrCodeDashboardImmutable, "system dashboards cannot be deleted")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dashboard *Dashboard) ErrIfNotLockable() error {
|
||||
if err := dashboard.ErrIfNotMutable(); err != nil {
|
||||
return err
|
||||
}
|
||||
if dashboard.Source == SourceSystem {
|
||||
return errors.Newf(errors.TypeInvalidInput, ErrCodeDashboardImmutable, "system dashboards cannot be locked or unlocked")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dashboard *Dashboard) ErrIfNotPublishable() error {
|
||||
if err := dashboard.ErrIfNotMutable(); err != nil {
|
||||
return err
|
||||
}
|
||||
if dashboard.Source == SourceSystem {
|
||||
return errors.Newf(errors.TypeInvalidInput, ErrCodeDashboardImmutable, "system dashboards cannot be made public")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dashboard *Dashboard) CanUpdate(ctx context.Context, data StorableDashboardData, diff int) error {
|
||||
if dashboard.Locked {
|
||||
return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "cannot update a locked dashboard, please unlock the dashboard to update")
|
||||
}
|
||||
|
||||
existingIDs := dashboard.Data.GetWidgetIds()
|
||||
newIDs := data.GetWidgetIds()
|
||||
newIdsMap := make(map[string]bool)
|
||||
for _, id := range newIDs {
|
||||
newIdsMap[id] = true
|
||||
}
|
||||
|
||||
differenceMap := make(map[string]bool)
|
||||
difference := []string{}
|
||||
for _, id := range existingIDs {
|
||||
if _, found := newIdsMap[id]; !found && !differenceMap[id] {
|
||||
difference = append(difference, id)
|
||||
differenceMap[id] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Allow multiple decisions only if diff == 0
|
||||
if diff > 0 && len(difference) > diff {
|
||||
return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "deleting more than one panel is not supported")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dashboard *Dashboard) Update(ctx context.Context, updatableDashboard UpdatableDashboard, updatedBy string, diff int) error {
|
||||
err := dashboard.CanUpdate(ctx, updatableDashboard, diff)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dashboard.UpdatedBy = updatedBy
|
||||
dashboard.UpdatedAt = time.Now()
|
||||
dashboard.Data = updatableDashboard
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dashboard *Dashboard) CanLockUnlock(isAdmin bool, updatedBy string) error {
|
||||
if dashboard.CreatedBy != updatedBy && !isAdmin {
|
||||
return errors.Newf(errors.TypeForbidden, errors.CodeForbidden, "you are not authorized to lock/unlock this dashboard")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dashboard *Dashboard) LockUnlock(lock bool, isAdmin bool, updatedBy string) error {
|
||||
err := dashboard.CanLockUnlock(isAdmin, updatedBy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dashboard.Locked = lock
|
||||
dashboard.UpdatedBy = updatedBy
|
||||
dashboard.UpdatedAt = time.Now()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (lockUnlockDashboard *LockUnlockDashboard) UnmarshalJSON(src []byte) error {
|
||||
var lockUnlock struct {
|
||||
Locked *bool `json:"lock"`
|
||||
}
|
||||
|
||||
err := json.Unmarshal(src, &lockUnlock)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if lockUnlock.Locked == nil {
|
||||
return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "lock is missing in the request payload")
|
||||
}
|
||||
|
||||
lockUnlockDashboard.Locked = lockUnlock.Locked
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dashboard *Dashboard) GetWidgetQuery(startTime, endTime, widgetIndex uint64, logger *slog.Logger) (*querybuildertypesv5.QueryRangeRequest, error) {
|
||||
type dashboardData struct {
|
||||
Widgets []struct {
|
||||
PanelTypes string `json:"panelTypes"`
|
||||
Query struct {
|
||||
Builder struct {
|
||||
QueryData []map[string]any `json:"queryData"`
|
||||
QueryFormulas []map[string]any `json:"queryFormulas"`
|
||||
QueryTraceOperator []map[string]any `json:"queryTraceOperator"`
|
||||
} `json:"builder"`
|
||||
ClickhouseSQL []map[string]any `json:"clickhouse_sql"`
|
||||
PromQL []map[string]any `json:"promql"`
|
||||
QueryType string `json:"queryType"`
|
||||
} `json:"query"`
|
||||
FillGaps bool `json:"fillSpans"`
|
||||
} `json:"widgets"`
|
||||
}
|
||||
|
||||
dataJSON, err := json.Marshal(dashboard.Data)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, errors.TypeInvalidInput, ErrCodeDashboardInvalidData, "invalid dashboard data")
|
||||
}
|
||||
|
||||
var data dashboardData
|
||||
err = json.Unmarshal(dataJSON, &data)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, errors.TypeInvalidInput, ErrCodeDashboardInvalidData, "invalid dashboard data")
|
||||
}
|
||||
|
||||
if int(widgetIndex) >= len(data.Widgets) {
|
||||
return nil, errors.Newf(errors.TypeInvalidInput, ErrCodeDashboardInvalidInput, "widget with index %v doesn't exist", widgetIndex)
|
||||
}
|
||||
|
||||
compositeQueries := []any{}
|
||||
widgetData := data.Widgets[widgetIndex]
|
||||
switch widgetData.Query.QueryType {
|
||||
case "builder":
|
||||
for _, query := range widgetData.Query.Builder.QueryData {
|
||||
queryName, ok := query["queryName"].(string)
|
||||
if !ok {
|
||||
return nil, errors.New(errors.TypeInvalidInput, ErrCodeDashboardInvalidWidgetQuery, "cannot type cast query name as string")
|
||||
}
|
||||
compositeQueries = append(compositeQueries, querybuildertypesv5.WrapInV5Envelope(queryName, query, "builder_query"))
|
||||
}
|
||||
for _, query := range widgetData.Query.Builder.QueryFormulas {
|
||||
queryName, ok := query["queryName"].(string)
|
||||
if !ok {
|
||||
return nil, errors.New(errors.TypeInvalidInput, ErrCodeDashboardInvalidWidgetQuery, "cannot type cast query name as string")
|
||||
}
|
||||
compositeQueries = append(compositeQueries, querybuildertypesv5.WrapInV5Envelope(queryName, query, "builder_formula"))
|
||||
}
|
||||
for _, query := range widgetData.Query.Builder.QueryTraceOperator {
|
||||
queryName, ok := query["queryName"].(string)
|
||||
if !ok {
|
||||
return nil, errors.New(errors.TypeInvalidInput, ErrCodeDashboardInvalidWidgetQuery, "cannot type cast query name as string")
|
||||
}
|
||||
compositeQueries = append(compositeQueries, querybuildertypesv5.WrapInV5Envelope(queryName, query, "builder_trace_operator"))
|
||||
}
|
||||
case "clickhouse_sql":
|
||||
for _, query := range widgetData.Query.ClickhouseSQL {
|
||||
envelope := map[string]any{
|
||||
"type": "clickhouse_sql",
|
||||
"spec": map[string]any{
|
||||
"name": query["name"],
|
||||
"query": query["query"],
|
||||
"disabled": query["disabled"],
|
||||
"legend": query["legend"],
|
||||
},
|
||||
}
|
||||
compositeQueries = append(compositeQueries, envelope)
|
||||
}
|
||||
case "promql":
|
||||
for _, query := range widgetData.Query.PromQL {
|
||||
envelope := map[string]any{
|
||||
"type": "promql",
|
||||
"spec": map[string]any{
|
||||
"name": query["name"],
|
||||
"query": query["query"],
|
||||
"disabled": query["disabled"],
|
||||
"legend": query["legend"],
|
||||
},
|
||||
}
|
||||
compositeQueries = append(compositeQueries, envelope)
|
||||
}
|
||||
}
|
||||
|
||||
queryRangeReq := map[string]any{
|
||||
"schemaVersion": "v1",
|
||||
"start": startTime,
|
||||
"end": endTime,
|
||||
"requestType": dashboard.getQueryRequestTypeFromPanelType(widgetData.PanelTypes),
|
||||
"compositeQuery": map[string]any{
|
||||
"queries": compositeQueries,
|
||||
},
|
||||
"formatOptions": map[string]any{
|
||||
"fillGaps": widgetData.FillGaps,
|
||||
"formatTableResultForUI": widgetData.PanelTypes == "table",
|
||||
},
|
||||
}
|
||||
|
||||
req, err := json.Marshal(queryRangeReq)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, errors.TypeInvalidInput, ErrCodeDashboardInvalidWidgetQuery, "invalid query request")
|
||||
}
|
||||
|
||||
queryRangeRequest := new(querybuildertypesv5.QueryRangeRequest)
|
||||
err = json.Unmarshal(req, queryRangeRequest)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, errors.TypeInvalidInput, ErrCodeDashboardInvalidWidgetQuery, "invalid query request")
|
||||
}
|
||||
|
||||
return queryRangeRequest, nil
|
||||
}
|
||||
|
||||
func (dashboard *Dashboard) getQueryRequestTypeFromPanelType(panelType string) querybuildertypesv5.RequestType {
|
||||
switch panelType {
|
||||
case "graph", "bar":
|
||||
return querybuildertypesv5.RequestTypeTimeSeries
|
||||
case "table", "pie", "value":
|
||||
return querybuildertypesv5.RequestTypeScalar
|
||||
case "trace":
|
||||
return querybuildertypesv5.RequestTypeTrace
|
||||
case "list":
|
||||
return querybuildertypesv5.RequestTypeRaw
|
||||
case "histogram":
|
||||
return querybuildertypesv5.RequestTypeDistribution
|
||||
}
|
||||
|
||||
return querybuildertypesv5.RequestTypeUnknown
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package dashboardtypes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
@@ -20,69 +19,6 @@ func makeTestWidgets(ids ...string) []interface{} {
|
||||
return widgets
|
||||
}
|
||||
|
||||
func TestCanUpdate_MultipleDeletions_ByDiff(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
diff int
|
||||
updated []string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "diff-0-allows-multi-delete",
|
||||
diff: 0,
|
||||
updated: []string{"a"}, // deleting 2 widgets (b, c)
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "diff-1-blocks-multi-delete",
|
||||
diff: 1,
|
||||
updated: []string{"a"}, // deleting 2 widgets (b, c) > diff(1)
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "diff-1-allows-single-delete",
|
||||
diff: 1,
|
||||
updated: []string{"a", "b"}, // deleting 1 widget (c) = diff(1)
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "diff-2-allows-two-deletions",
|
||||
diff: 2,
|
||||
updated: []string{"a"}, // deleting 2 widgets (b, c) = diff(2)
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "diff-1-blocks-three-deletions",
|
||||
diff: 1,
|
||||
updated: []string{}, // deleting all 3 widgets > diff(1)
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
orgID := valuer.GenerateUUID()
|
||||
initial := StorableDashboardData{
|
||||
"widgets": makeTestWidgets("a", "b", "c"),
|
||||
}
|
||||
d, err := NewDashboard(orgID, "tester", SourceUser, initial)
|
||||
assert.NoError(t, err)
|
||||
|
||||
updated := StorableDashboardData{
|
||||
"widgets": makeTestWidgets(tc.updated...),
|
||||
}
|
||||
err = d.CanUpdate(ctx, updated, tc.diff)
|
||||
if tc.wantErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStorableDashboardErrIfNotDeletable(t *testing.T) {
|
||||
testCases := []struct {
|
||||
subtestName string
|
||||
|
||||
@@ -95,6 +95,13 @@ func (d *DashboardV2) ErrIfNotUpdatable() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DashboardV2) ErrIfNotPublishable() error {
|
||||
if d.Source == SourceSystem {
|
||||
return errors.Newf(errors.TypeInvalidInput, ErrCodeDashboardImmutable, "system dashboards cannot be made public")
|
||||
}
|
||||
return d.ErrIfNotMutable()
|
||||
}
|
||||
|
||||
func (d *DashboardV2) Update(updatable UpdatableDashboardV2, updatedBy string, resolvedTags []*tagtypes.Tag) error {
|
||||
if err := d.ErrIfNotUpdatable(); err != nil {
|
||||
return err
|
||||
|
||||
@@ -53,11 +53,6 @@ type UpdatablePublicDashboard struct {
|
||||
DefaultTimeRange string `json:"defaultTimeRange"`
|
||||
}
|
||||
|
||||
type GettablePublicDashboardData struct {
|
||||
Dashboard *Dashboard `json:"dashboard"`
|
||||
PublicDashboard *GettablePublicDasbhboard `json:"publicDashboard"`
|
||||
}
|
||||
|
||||
func NewPublicDashboard(timeRangeEnabled bool, defaultTimeRange string, dashboardID valuer.UUID) *PublicDashboard {
|
||||
return &PublicDashboard{
|
||||
Identifiable: types.Identifiable{
|
||||
@@ -101,108 +96,6 @@ func NewGettablePublicDashboard(publicDashboard *PublicDashboard) *GettablePubli
|
||||
}
|
||||
}
|
||||
|
||||
func NewPublicDashboardDataFromDashboard(dashboard *Dashboard, publicDashboard *PublicDashboard) (*GettablePublicDashboardData, error) {
|
||||
type dashboardData struct {
|
||||
Widgets []struct {
|
||||
PanelTypes string `json:"panelTypes"`
|
||||
Query struct {
|
||||
Builder struct {
|
||||
QueryData []map[string]any `json:"queryData"`
|
||||
QueryFormulas []map[string]any `json:"queryFormulas"`
|
||||
QueryTraceOperator []map[string]any `json:"queryTraceOperator"`
|
||||
} `json:"builder"`
|
||||
ClickhouseSQL []map[string]any `json:"clickhouse_sql"`
|
||||
PromQL []map[string]any `json:"promql"`
|
||||
QueryType string `json:"queryType"`
|
||||
} `json:"query"`
|
||||
} `json:"widgets"`
|
||||
}
|
||||
|
||||
dataJSON, err := json.Marshal(dashboard.Data)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, errors.TypeInvalidInput, ErrCodeDashboardInvalidData, "invalid dashboard data")
|
||||
}
|
||||
|
||||
var data dashboardData
|
||||
err = json.Unmarshal(dataJSON, &data)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, errors.TypeInvalidInput, ErrCodeDashboardInvalidData, "invalid dashboard data")
|
||||
}
|
||||
|
||||
for idx, widget := range data.Widgets {
|
||||
updatedQueryData := []map[string]any{}
|
||||
for _, queryData := range widget.Query.Builder.QueryData {
|
||||
updatedQueryMap := map[string]any{}
|
||||
updatedQueryMap["aggregations"] = queryData["aggregations"]
|
||||
updatedQueryMap["legend"] = queryData["legend"]
|
||||
updatedQueryMap["queryName"] = queryData["queryName"]
|
||||
updatedQueryMap["expression"] = queryData["expression"]
|
||||
updatedQueryMap["groupBy"] = queryData["groupBy"]
|
||||
updatedQueryMap["dataSource"] = queryData["dataSource"]
|
||||
updatedQueryData = append(updatedQueryData, updatedQueryMap)
|
||||
}
|
||||
widget.Query.Builder.QueryData = updatedQueryData
|
||||
|
||||
updatedQueryFormulas := []map[string]any{}
|
||||
for _, queryFormula := range widget.Query.Builder.QueryFormulas {
|
||||
updatedQueryFormulaMap := map[string]any{}
|
||||
updatedQueryFormulaMap["legend"] = queryFormula["legend"]
|
||||
updatedQueryFormulaMap["queryName"] = queryFormula["queryName"]
|
||||
updatedQueryFormulaMap["expression"] = queryFormula["expression"]
|
||||
updatedQueryFormulas = append(updatedQueryFormulas, updatedQueryFormulaMap)
|
||||
}
|
||||
widget.Query.Builder.QueryFormulas = updatedQueryFormulas
|
||||
|
||||
updatedQueryTraceOperator := []map[string]any{}
|
||||
for _, queryTraceOperator := range widget.Query.Builder.QueryTraceOperator {
|
||||
updatedQueryTraceOperatorMap := map[string]any{}
|
||||
updatedQueryTraceOperatorMap["aggregations"] = queryTraceOperator["aggregations"]
|
||||
updatedQueryTraceOperatorMap["legend"] = queryTraceOperator["legend"]
|
||||
updatedQueryTraceOperatorMap["queryName"] = queryTraceOperator["queryName"]
|
||||
updatedQueryTraceOperatorMap["expression"] = queryTraceOperator["expression"]
|
||||
updatedQueryTraceOperatorMap["groupBy"] = queryTraceOperator["groupBy"]
|
||||
updatedQueryTraceOperatorMap["dataSource"] = queryTraceOperator["dataSource"]
|
||||
updatedQueryTraceOperator = append(updatedQueryTraceOperator, updatedQueryTraceOperatorMap)
|
||||
}
|
||||
widget.Query.Builder.QueryTraceOperator = updatedQueryTraceOperator
|
||||
|
||||
updatedClickhouseSQLQuery := []map[string]any{}
|
||||
for _, clickhouseSQLQuery := range widget.Query.ClickhouseSQL {
|
||||
updatedClickhouseSQLQueryMap := make(map[string]any)
|
||||
updatedClickhouseSQLQueryMap["legend"] = clickhouseSQLQuery["legend"]
|
||||
updatedClickhouseSQLQueryMap["name"] = clickhouseSQLQuery["name"]
|
||||
updatedClickhouseSQLQuery = append(updatedClickhouseSQLQuery, updatedClickhouseSQLQueryMap)
|
||||
}
|
||||
widget.Query.ClickhouseSQL = updatedClickhouseSQLQuery
|
||||
|
||||
updatedPromQLQuery := []map[string]any{}
|
||||
for _, promQLQuery := range widget.Query.PromQL {
|
||||
updatedPromQLQueryMap := make(map[string]any)
|
||||
updatedPromQLQueryMap["legend"] = promQLQuery["legend"]
|
||||
updatedPromQLQueryMap["name"] = promQLQuery["name"]
|
||||
updatedPromQLQuery = append(updatedPromQLQuery, updatedPromQLQueryMap)
|
||||
}
|
||||
widget.Query.PromQL = updatedPromQLQuery
|
||||
|
||||
if widgets, ok := dashboard.Data["widgets"].([]any); ok {
|
||||
if widgetMap, ok := widgets[idx].(map[string]any); ok {
|
||||
widgetMap["query"] = widget.Query
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &GettablePublicDashboardData{
|
||||
Dashboard: &Dashboard{
|
||||
Data: dashboard.Data,
|
||||
},
|
||||
PublicDashboard: &GettablePublicDasbhboard{
|
||||
TimeRangeEnabled: publicDashboard.TimeRangeEnabled,
|
||||
DefaultTimeRange: publicDashboard.DefaultTimeRange,
|
||||
PublicPath: publicDashboard.PublicPath(),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (typ *PublicDashboard) Update(timeRangeEnabled bool, defaultTimeRange string) {
|
||||
typ.TimeRangeEnabled = timeRangeEnabled
|
||||
typ.DefaultTimeRange = defaultTimeRange
|
||||
|
||||
@@ -50,21 +50,17 @@ func TestErrIfNotMutable_BySource(t *testing.T) {
|
||||
cases := []struct {
|
||||
source Source
|
||||
mutable bool
|
||||
deletable bool
|
||||
lockable bool
|
||||
publishable bool
|
||||
}{
|
||||
{SourceUser, true, true, true, true},
|
||||
{SourceSystem, true, false, false, false},
|
||||
{SourceIntegration, false, false, false, false},
|
||||
{SourceUser, true, true},
|
||||
{SourceSystem, false, false},
|
||||
{SourceIntegration, false, false},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.source.StringValue(), func(t *testing.T) {
|
||||
d := &Dashboard{Source: tc.source}
|
||||
d := &DashboardV2{Source: tc.source}
|
||||
assert.Equal(t, tc.mutable, d.ErrIfNotMutable() == nil)
|
||||
assert.Equal(t, tc.deletable, d.ErrIfNotDeletable() == nil)
|
||||
assert.Equal(t, tc.lockable, d.ErrIfNotLockable() == nil)
|
||||
assert.Equal(t, tc.publishable, d.ErrIfNotPublishable() == nil)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
package dashboardtypes
|
||||
|
||||
import "github.com/SigNoz/signoz/pkg/errors"
|
||||
|
||||
// V2DashboardAPIDocsLink documents the v2 dashboard API — its endpoints and request bodies.
|
||||
const V2DashboardAPIDocsLink = "https://signoz.io/docs/dashboards/dashboards-v2-api/"
|
||||
|
||||
// NewV1DeprecatedError builds the error a deprecated v1 dashboard endpoint returns.
|
||||
// useInstead names the v2 endpoint to call; the docs link is appended for the details.
|
||||
func NewV1DeprecatedError(useInstead string) error {
|
||||
return errors.Newf(errors.TypeUnsupported, ErrCodeDashboardV1Deprecated,
|
||||
"the v1 dashboard API is deprecated; instead, %s. See %s", useInstead, V2DashboardAPIDocsLink)
|
||||
}
|
||||
@@ -240,19 +240,6 @@ type MetricAlertsResponse struct {
|
||||
Alerts []MetricAlert `json:"alerts" required:"true" nullable:"true"`
|
||||
}
|
||||
|
||||
// MetricDashboard represents a dashboard/widget referencing a metric.
|
||||
type MetricDashboard struct {
|
||||
DashboardName string `json:"dashboardName" required:"true"`
|
||||
DashboardID string `json:"dashboardId" required:"true"`
|
||||
WidgetID string `json:"widgetId" required:"true"`
|
||||
WidgetName string `json:"widgetName" required:"true"`
|
||||
}
|
||||
|
||||
// MetricDashboardsResponse represents the response for metric dashboards endpoint.
|
||||
type MetricDashboardsResponse struct {
|
||||
Dashboards []MetricDashboard `json:"dashboards" required:"true" nullable:"true"`
|
||||
}
|
||||
|
||||
// MetricDashboardPanelsResponse is the response for the v2 metric dashboards
|
||||
// endpoint: the dashboard panels that reference the metric.
|
||||
type MetricDashboardPanelsResponse struct {
|
||||
|
||||
@@ -38,18 +38,3 @@ var alertStateSeverity = map[AlertState]int{
|
||||
func (a AlertState) Severity() int {
|
||||
return alertStateSeverity[a]
|
||||
}
|
||||
|
||||
// Display priority for list sorting, worst first from a user's view; deliberately
|
||||
// NOT Severity(), which ranks disabled/nodata above firing for overall-state computation.
|
||||
var alertStateDisplayRank = map[AlertState]int{
|
||||
StateFiring: 5,
|
||||
StateNoData: 4,
|
||||
StatePending: 3,
|
||||
StateRecovering: 2,
|
||||
StateInactive: 1,
|
||||
StateDisabled: 0,
|
||||
}
|
||||
|
||||
func (a AlertState) DisplayRank() int {
|
||||
return alertStateDisplayRank[a]
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
package ruletypes
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// Both rankings must stay exhaustive: a new AlertState needs an entry in each.
|
||||
func TestAlertStateRankingsAreExhaustive(t *testing.T) {
|
||||
states := AlertState{}.Enum()
|
||||
|
||||
assert.Len(t, alertStateSeverity, len(states))
|
||||
assert.Len(t, alertStateDisplayRank, len(states))
|
||||
|
||||
for _, s := range states {
|
||||
state := s.(AlertState)
|
||||
assert.Contains(t, alertStateSeverity, state, "missing severity for state %q", state)
|
||||
assert.Contains(t, alertStateDisplayRank, state, "missing display rank for state %q", state)
|
||||
}
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
package ruletypes
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultListLimit = 20
|
||||
MaxListLimit = 200
|
||||
MaxListQueryLen = 1024
|
||||
)
|
||||
|
||||
var ErrCodeRuleListInvalid = errors.MustNewCode("rule_list_invalid")
|
||||
|
||||
type ListSort struct{ valuer.String }
|
||||
|
||||
var (
|
||||
ListSortUpdatedAt = ListSort{valuer.NewString("updated_at")}
|
||||
ListSortCreatedAt = ListSort{valuer.NewString("created_at")}
|
||||
ListSortName = ListSort{valuer.NewString("name")}
|
||||
ListSortState = ListSort{valuer.NewString("state")}
|
||||
ListSortSeverity = ListSort{valuer.NewString("severity")}
|
||||
)
|
||||
|
||||
func (ListSort) Enum() []any {
|
||||
return []any{ListSortUpdatedAt, ListSortCreatedAt, ListSortName, ListSortState, ListSortSeverity}
|
||||
}
|
||||
|
||||
func (s ListSort) IsValid() bool {
|
||||
return slices.ContainsFunc(s.Enum(), func(v any) bool { return v == s })
|
||||
}
|
||||
|
||||
type ListOrder struct{ valuer.String }
|
||||
|
||||
var (
|
||||
ListOrderAsc = ListOrder{valuer.NewString("asc")}
|
||||
ListOrderDesc = ListOrder{valuer.NewString("desc")}
|
||||
)
|
||||
|
||||
func (ListOrder) Enum() []any {
|
||||
return []any{ListOrderAsc, ListOrderDesc}
|
||||
}
|
||||
|
||||
func (o ListOrder) IsValid() bool {
|
||||
return slices.ContainsFunc(o.Enum(), func(v any) bool { return v == o })
|
||||
}
|
||||
|
||||
// ListFilter is the rule listing state shared by the v3 list params and saved views.
|
||||
type ListFilter struct {
|
||||
Query string `query:"query" json:"query"`
|
||||
// gin cannot bind a slice of valuer enums; GetAlertStates converts these.
|
||||
States []string `query:"states" json:"states" nullable:"false"`
|
||||
Sort ListSort `query:"sort" json:"sort"`
|
||||
Order ListOrder `query:"order" json:"order"`
|
||||
}
|
||||
|
||||
// Validate normalizes in place; zero sort/order get the defaults, nil states an empty slice.
|
||||
func (f *ListFilter) Validate() error {
|
||||
if f.States == nil {
|
||||
f.States = []string{}
|
||||
}
|
||||
|
||||
if n := utf8.RuneCountInString(f.Query); n > MaxListQueryLen {
|
||||
return errors.NewInvalidInputf(ErrCodeRuleListInvalid,
|
||||
"query cannot be longer than %d characters, got %d", MaxListQueryLen, n)
|
||||
}
|
||||
|
||||
if _, err := f.GetAlertStates(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if f.Sort.IsZero() {
|
||||
f.Sort = ListSortUpdatedAt
|
||||
} else if !f.Sort.IsValid() {
|
||||
return errors.NewInvalidInputf(ErrCodeRuleListInvalid,
|
||||
"invalid sort %q, expected one of: `updated_at`, `created_at`, `name`, `state`, `severity`", f.Sort)
|
||||
}
|
||||
|
||||
if f.Order.IsZero() {
|
||||
f.Order = ListOrderDesc
|
||||
} else if !f.Order.IsValid() {
|
||||
return errors.NewInvalidInputf(ErrCodeRuleListInvalid,
|
||||
"invalid order %q, expected `asc` or `desc`", f.Order)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAlertStates parses States; empty means no state filtering.
|
||||
func (f *ListFilter) GetAlertStates() ([]AlertState, error) {
|
||||
if len(f.States) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
states := make([]AlertState, 0, len(f.States))
|
||||
for _, raw := range f.States {
|
||||
state, err := parseAlertState(raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
states = append(states, state)
|
||||
}
|
||||
|
||||
return states, nil
|
||||
}
|
||||
|
||||
func parseAlertState(raw string) (AlertState, error) {
|
||||
state := AlertState{valuer.NewString(raw)}
|
||||
if !slices.Contains(state.Enum(), any(state)) {
|
||||
return AlertState{}, errors.NewInvalidInputf(ErrCodeRuleListInvalid,
|
||||
"invalid state %q, expected one of: `firing`, `pending`, `recovering`, `inactive`, `nodata`, `disabled`", raw)
|
||||
}
|
||||
return state, nil
|
||||
}
|
||||
|
||||
type ListRulesParams struct {
|
||||
ListFilter
|
||||
Limit int `query:"limit"`
|
||||
Offset int `query:"offset"`
|
||||
}
|
||||
|
||||
// Validate normalizes in place; an over-max limit is clamped, not rejected.
|
||||
func (p *ListRulesParams) Validate() error {
|
||||
if err := p.ListFilter.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if p.Limit == 0 {
|
||||
p.Limit = DefaultListLimit
|
||||
} else if p.Limit < 0 {
|
||||
return errors.NewInvalidInputf(ErrCodeRuleListInvalid,
|
||||
"invalid limit %d, must be a positive integer", p.Limit)
|
||||
} else if p.Limit > MaxListLimit {
|
||||
p.Limit = MaxListLimit
|
||||
}
|
||||
|
||||
if p.Offset < 0 {
|
||||
return errors.NewInvalidInputf(ErrCodeRuleListInvalid,
|
||||
"invalid offset %d, must be a non-negative integer", p.Offset)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
package ruletypes
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
qbtypesv5 "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
)
|
||||
|
||||
var ErrCodeRuleListFilterInvalid = errors.MustNewCode("rule_list_filter_invalid")
|
||||
|
||||
// DSLKey is a reserved (column-level) key in the rule list filter DSL.
|
||||
type DSLKey string
|
||||
|
||||
const (
|
||||
DSLKeyName DSLKey = "name"
|
||||
DSLKeySeverity DSLKey = "severity"
|
||||
DSLKeyCreatedBy DSLKey = "created_by"
|
||||
DSLKeyUpdatedBy DSLKey = "updated_by"
|
||||
DSLKeyCreatedAt DSLKey = "created_at"
|
||||
DSLKeyUpdatedAt DSLKey = "updated_at"
|
||||
DSLKeyAlertType DSLKey = "alert_type"
|
||||
DSLKeyRuleType DSLKey = "rule_type"
|
||||
|
||||
// Label keys under this prefix are matched exactly (case-sensitive).
|
||||
DSLLabelsKeyPrefix = "labels."
|
||||
|
||||
// Advertised in reservedKeywords; not itself a filterable key.
|
||||
DSLKeyLabelsPlaceholder DSLKey = "labels.<key>"
|
||||
)
|
||||
|
||||
func ReservedFilterKeys() []DSLKey {
|
||||
keys := make([]DSLKey, 0, len(ReservedOps)+1)
|
||||
for key := range ReservedOps {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
keys = append(keys, DSLKeyLabelsPlaceholder)
|
||||
slices.SortFunc(keys, func(a, b DSLKey) int {
|
||||
return strings.Compare(string(a), string(b))
|
||||
})
|
||||
return keys
|
||||
}
|
||||
|
||||
// ReservedOps lists the operators each reserved DSL key accepts; `labels.<key>` terms use LabelsKeyOps.
|
||||
var ReservedOps = map[DSLKey]map[qbtypesv5.FilterOperator]struct{}{
|
||||
DSLKeyName: stringSearchOps(),
|
||||
// severity aliases labels.severity, so it takes the labels operator set.
|
||||
DSLKeySeverity: LabelsKeyOps,
|
||||
DSLKeyCreatedBy: stringSearchOps(),
|
||||
DSLKeyUpdatedBy: stringSearchOps(),
|
||||
DSLKeyCreatedAt: numericRangeOps(),
|
||||
DSLKeyUpdatedAt: numericRangeOps(),
|
||||
DSLKeyAlertType: enumOps(),
|
||||
DSLKeyRuleType: enumOps(),
|
||||
}
|
||||
|
||||
// LabelsKeyOps operators target the label's value; EXISTS/NOT EXISTS test its presence.
|
||||
var LabelsKeyOps = opsSet(
|
||||
qbtypesv5.FilterOperatorEqual, qbtypesv5.FilterOperatorNotEqual,
|
||||
qbtypesv5.FilterOperatorLike, qbtypesv5.FilterOperatorNotLike,
|
||||
qbtypesv5.FilterOperatorILike, qbtypesv5.FilterOperatorNotILike,
|
||||
qbtypesv5.FilterOperatorContains, qbtypesv5.FilterOperatorNotContains,
|
||||
qbtypesv5.FilterOperatorIn, qbtypesv5.FilterOperatorNotIn,
|
||||
qbtypesv5.FilterOperatorExists, qbtypesv5.FilterOperatorNotExists,
|
||||
)
|
||||
|
||||
func stringSearchOps() map[qbtypesv5.FilterOperator]struct{} {
|
||||
return opsSet(
|
||||
qbtypesv5.FilterOperatorEqual, qbtypesv5.FilterOperatorNotEqual,
|
||||
qbtypesv5.FilterOperatorLike, qbtypesv5.FilterOperatorNotLike,
|
||||
qbtypesv5.FilterOperatorILike, qbtypesv5.FilterOperatorNotILike,
|
||||
qbtypesv5.FilterOperatorContains, qbtypesv5.FilterOperatorNotContains,
|
||||
qbtypesv5.FilterOperatorIn, qbtypesv5.FilterOperatorNotIn,
|
||||
)
|
||||
}
|
||||
|
||||
func numericRangeOps() map[qbtypesv5.FilterOperator]struct{} {
|
||||
return opsSet(
|
||||
qbtypesv5.FilterOperatorEqual, qbtypesv5.FilterOperatorNotEqual,
|
||||
qbtypesv5.FilterOperatorLessThan, qbtypesv5.FilterOperatorLessThanOrEq,
|
||||
qbtypesv5.FilterOperatorGreaterThan, qbtypesv5.FilterOperatorGreaterThanOrEq,
|
||||
qbtypesv5.FilterOperatorBetween, qbtypesv5.FilterOperatorNotBetween,
|
||||
)
|
||||
}
|
||||
|
||||
func enumOps() map[qbtypesv5.FilterOperator]struct{} {
|
||||
return opsSet(
|
||||
qbtypesv5.FilterOperatorEqual, qbtypesv5.FilterOperatorNotEqual,
|
||||
qbtypesv5.FilterOperatorIn, qbtypesv5.FilterOperatorNotIn,
|
||||
)
|
||||
}
|
||||
|
||||
func opsSet(ops ...qbtypesv5.FilterOperator) map[qbtypesv5.FilterOperator]struct{} {
|
||||
m := make(map[qbtypesv5.FilterOperator]struct{}, len(ops))
|
||||
for _, op := range ops {
|
||||
m[op] = struct{}{}
|
||||
}
|
||||
return m
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
package ruletypes
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
qbtypesv5 "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestReservedFilterKeys(t *testing.T) {
|
||||
assert.Equal(t, []DSLKey{
|
||||
DSLKeyAlertType,
|
||||
DSLKeyCreatedAt,
|
||||
DSLKeyCreatedBy,
|
||||
DSLKeyLabelsPlaceholder,
|
||||
DSLKeyName,
|
||||
DSLKeyRuleType,
|
||||
DSLKeySeverity,
|
||||
DSLKeyUpdatedAt,
|
||||
DSLKeyUpdatedBy,
|
||||
}, ReservedFilterKeys())
|
||||
}
|
||||
|
||||
func TestFilterOpsExcludeRegexp(t *testing.T) {
|
||||
for key, ops := range ReservedOps {
|
||||
assert.NotEmpty(t, ops, "key %q has no operators", key)
|
||||
assert.NotContains(t, ops, qbtypesv5.FilterOperatorRegexp, "key %q allows REGEXP", key)
|
||||
assert.NotContains(t, ops, qbtypesv5.FilterOperatorNotRegexp, "key %q allows NOT REGEXP", key)
|
||||
}
|
||||
assert.NotContains(t, LabelsKeyOps, qbtypesv5.FilterOperatorRegexp)
|
||||
assert.NotContains(t, LabelsKeyOps, qbtypesv5.FilterOperatorNotRegexp)
|
||||
assert.Contains(t, LabelsKeyOps, qbtypesv5.FilterOperatorExists)
|
||||
assert.Contains(t, LabelsKeyOps, qbtypesv5.FilterOperatorNotExists)
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
package ruletypes
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestListRulesParamsValidate(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
params ListRulesParams
|
||||
wantErr string
|
||||
wantSort ListSort
|
||||
wantOrder ListOrder
|
||||
wantLimit int
|
||||
}{
|
||||
{
|
||||
name: "empty params get defaults",
|
||||
params: ListRulesParams{},
|
||||
wantSort: ListSortUpdatedAt,
|
||||
wantOrder: ListOrderDesc,
|
||||
wantLimit: DefaultListLimit,
|
||||
},
|
||||
{
|
||||
name: "explicit values kept",
|
||||
params: ListRulesParams{ListFilter: ListFilter{Sort: ListSortSeverity, Order: ListOrderAsc}, Limit: 50, Offset: 100},
|
||||
wantSort: ListSortSeverity,
|
||||
wantOrder: ListOrderAsc,
|
||||
wantLimit: 50,
|
||||
},
|
||||
{
|
||||
name: "over-max limit clamped",
|
||||
params: ListRulesParams{Limit: MaxListLimit + 1},
|
||||
wantSort: ListSortUpdatedAt,
|
||||
wantOrder: ListOrderDesc,
|
||||
wantLimit: MaxListLimit,
|
||||
},
|
||||
{
|
||||
name: "invalid state rejected",
|
||||
params: ListRulesParams{ListFilter: ListFilter{States: []string{"bogus"}}},
|
||||
wantErr: `invalid state "bogus"`,
|
||||
},
|
||||
{
|
||||
name: "invalid sort rejected",
|
||||
params: ListRulesParams{ListFilter: ListFilter{Sort: ListSort{valuer.NewString("bogus")}}},
|
||||
wantErr: "invalid sort",
|
||||
},
|
||||
{
|
||||
name: "invalid order rejected",
|
||||
params: ListRulesParams{ListFilter: ListFilter{Order: ListOrder{valuer.NewString("bogus")}}},
|
||||
wantErr: "invalid order",
|
||||
},
|
||||
{
|
||||
name: "negative limit rejected",
|
||||
params: ListRulesParams{Limit: -1},
|
||||
wantErr: "invalid limit",
|
||||
},
|
||||
{
|
||||
name: "negative offset rejected",
|
||||
params: ListRulesParams{Offset: -1},
|
||||
wantErr: "invalid offset",
|
||||
},
|
||||
{
|
||||
name: "over-long query rejected",
|
||||
params: ListRulesParams{ListFilter: ListFilter{Query: strings.Repeat("a", MaxListQueryLen+1)}},
|
||||
wantErr: "query cannot be longer",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := tc.params.Validate()
|
||||
if tc.wantErr != "" {
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), tc.wantErr)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tc.wantSort, tc.params.Sort)
|
||||
assert.Equal(t, tc.wantOrder, tc.params.Order)
|
||||
assert.Equal(t, tc.wantLimit, tc.params.Limit)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestListRulesParamsAlertStates(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
states []string
|
||||
wantErr string
|
||||
wantStates []AlertState
|
||||
}{
|
||||
{
|
||||
name: "valid states parsed to typed values",
|
||||
states: []string{"firing", "pending"},
|
||||
wantStates: []AlertState{StateFiring, StatePending},
|
||||
},
|
||||
{
|
||||
name: "absent states mean no filtering",
|
||||
states: nil,
|
||||
},
|
||||
{
|
||||
name: "invalid state rejected",
|
||||
states: []string{"bogus"},
|
||||
wantErr: `invalid state "bogus"`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
params := ListRulesParams{ListFilter: ListFilter{States: tc.states}}
|
||||
states, err := params.GetAlertStates()
|
||||
if tc.wantErr != "" {
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), tc.wantErr)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tc.wantStates, states)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,190 +0,0 @@
|
||||
package ruletypes
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"encoding/json"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
)
|
||||
|
||||
const MaxListLabelPairs = 1000
|
||||
|
||||
// ListableRule is the slim per-row shape of the list endpoint; the full rule stays behind get-by-id.
|
||||
type ListableRule struct {
|
||||
Id string `json:"id" required:"true"`
|
||||
State AlertState `json:"state" required:"true"`
|
||||
AlertName string `json:"alert" required:"true"`
|
||||
Description string `json:"description,omitempty"`
|
||||
AlertType AlertType `json:"alertType" required:"true"`
|
||||
RuleType RuleType `json:"ruleType" required:"true"`
|
||||
Disabled bool `json:"disabled"`
|
||||
Labels map[string]string `json:"labels,omitempty"`
|
||||
types.TimeAuditable
|
||||
types.UserAuditable
|
||||
}
|
||||
|
||||
// storedRuleData is the subset of the persisted rule data blob the list page needs.
|
||||
type storedRuleData struct {
|
||||
AlertName string `json:"alert"`
|
||||
Description string `json:"description"`
|
||||
AlertType AlertType `json:"alertType"`
|
||||
RuleType RuleType `json:"ruleType"`
|
||||
Disabled bool `json:"disabled"`
|
||||
Labels map[string]string `json:"labels"`
|
||||
}
|
||||
|
||||
// ToListableRule leaves State zero; the caller overlays evaluation state.
|
||||
func (rule *StorableRule) ToListableRule() (*ListableRule, error) {
|
||||
data := storedRuleData{}
|
||||
if err := json.Unmarshal([]byte(rule.Data), &data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ListableRule{
|
||||
Id: rule.ID.StringValue(),
|
||||
AlertName: data.AlertName,
|
||||
Description: data.Description,
|
||||
AlertType: data.AlertType,
|
||||
RuleType: data.RuleType,
|
||||
Disabled: data.Disabled,
|
||||
Labels: data.Labels,
|
||||
TimeAuditable: rule.TimeAuditable,
|
||||
UserAuditable: rule.UserAuditable,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewListableRulesFromStorableRules converts rows, overlays evaluation state (absent means
|
||||
// disabled) and applies the state filter; corrupt rows come back keyed by rule id for the
|
||||
// caller to log.
|
||||
func NewListableRulesFromStorableRules(storedRules []*StorableRule, stateByRuleID map[string]AlertState, stateFilter map[AlertState]struct{}) ([]*ListableRule, map[string]error) {
|
||||
listableRules := make([]*ListableRule, 0, len(storedRules))
|
||||
errByRuleID := make(map[string]error)
|
||||
|
||||
for _, rule := range storedRules {
|
||||
listable, err := rule.ToListableRule()
|
||||
if err != nil {
|
||||
errByRuleID[rule.ID.StringValue()] = err
|
||||
continue
|
||||
}
|
||||
|
||||
if state, ok := stateByRuleID[listable.Id]; ok {
|
||||
listable.State = state
|
||||
} else {
|
||||
listable.State = StateDisabled
|
||||
listable.Disabled = true
|
||||
}
|
||||
if len(stateFilter) > 0 {
|
||||
if _, ok := stateFilter[listable.State]; !ok {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
listableRules = append(listableRules, listable)
|
||||
}
|
||||
|
||||
return listableRules, errByRuleID
|
||||
}
|
||||
|
||||
// LabelPair is one distinct label key/value observed on the org's rules.
|
||||
type LabelPair struct {
|
||||
Key string `json:"key" required:"true"`
|
||||
Value string `json:"value" required:"true"`
|
||||
}
|
||||
|
||||
type ListableRules struct {
|
||||
Rules []*ListableRule `json:"rules" required:"true" nullable:"false"`
|
||||
Total int64 `json:"total" required:"true"`
|
||||
Labels []LabelPair `json:"labels" required:"true" nullable:"false"`
|
||||
ReservedKeywords []DSLKey `json:"reservedKeywords" required:"true" nullable:"false"`
|
||||
}
|
||||
|
||||
func NewListableRules(rules []*ListableRule, total int64, labels []LabelPair) *ListableRules {
|
||||
return &ListableRules{
|
||||
Rules: rules,
|
||||
Total: total,
|
||||
Labels: labels,
|
||||
ReservedKeywords: ReservedFilterKeys(),
|
||||
}
|
||||
}
|
||||
|
||||
var severityDisplayRank = map[string]int{
|
||||
"critical": 4,
|
||||
"error": 3,
|
||||
"warning": 2,
|
||||
"info": 1,
|
||||
}
|
||||
|
||||
// Ties break on name then id ascending (order applies to the primary key only) so pages stay stable.
|
||||
func SortListableRules(rules []*ListableRule, sortBy ListSort, order ListOrder) {
|
||||
direction := 1
|
||||
if order == ListOrderDesc {
|
||||
direction = -1
|
||||
}
|
||||
slices.SortStableFunc(rules, func(a, b *ListableRule) int {
|
||||
if c := direction * compareListableRules(a, b, sortBy); c != 0 {
|
||||
return c
|
||||
}
|
||||
if c := strings.Compare(strings.ToLower(a.AlertName), strings.ToLower(b.AlertName)); c != 0 {
|
||||
return c
|
||||
}
|
||||
return strings.Compare(a.Id, b.Id)
|
||||
})
|
||||
}
|
||||
|
||||
func compareListableRules(a, b *ListableRule, sortBy ListSort) int {
|
||||
switch sortBy {
|
||||
case ListSortName:
|
||||
return strings.Compare(strings.ToLower(a.AlertName), strings.ToLower(b.AlertName))
|
||||
case ListSortCreatedAt:
|
||||
return a.CreatedAt.Compare(b.CreatedAt)
|
||||
case ListSortState:
|
||||
return cmp.Compare(a.State.DisplayRank(), b.State.DisplayRank())
|
||||
case ListSortSeverity:
|
||||
severityA := a.Labels["severity"]
|
||||
severityB := b.Labels["severity"]
|
||||
rankA := severityDisplayRank[strings.ToLower(severityA)]
|
||||
rankB := severityDisplayRank[strings.ToLower(severityB)]
|
||||
if rankA != rankB {
|
||||
return cmp.Compare(rankA, rankB)
|
||||
}
|
||||
if rankA == 0 {
|
||||
return strings.Compare(strings.ToLower(severityA), strings.ToLower(severityB))
|
||||
}
|
||||
return 0
|
||||
}
|
||||
return a.UpdatedAt.Compare(b.UpdatedAt)
|
||||
}
|
||||
|
||||
// NewLabelPairsFromRawJSON skips blank or malformed entries and caps the result at limit.
|
||||
func NewLabelPairsFromRawJSON(raws []string, limit int) []LabelPair {
|
||||
set := make(map[LabelPair]struct{})
|
||||
for _, raw := range raws {
|
||||
if raw == "" || raw == "null" {
|
||||
continue
|
||||
}
|
||||
labels := make(map[string]string)
|
||||
if err := json.Unmarshal([]byte(raw), &labels); err != nil {
|
||||
continue
|
||||
}
|
||||
for key, value := range labels {
|
||||
set[LabelPair{Key: key, Value: value}] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
pairs := make([]LabelPair, 0, len(set))
|
||||
for pair := range set {
|
||||
pairs = append(pairs, pair)
|
||||
}
|
||||
slices.SortFunc(pairs, func(a, b LabelPair) int {
|
||||
if c := strings.Compare(a.Key, b.Key); c != 0 {
|
||||
return c
|
||||
}
|
||||
return strings.Compare(a.Value, b.Value)
|
||||
})
|
||||
if len(pairs) > limit {
|
||||
pairs = pairs[:limit]
|
||||
}
|
||||
return pairs
|
||||
}
|
||||
@@ -1,261 +0,0 @@
|
||||
package ruletypes
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func listableRule(name string, state AlertState, severity string, updatedAt time.Time) *ListableRule {
|
||||
rule := &ListableRule{
|
||||
AlertName: name,
|
||||
State: state,
|
||||
TimeAuditable: types.TimeAuditable{
|
||||
UpdatedAt: updatedAt,
|
||||
},
|
||||
}
|
||||
if severity != "" {
|
||||
rule.Labels = map[string]string{"severity": severity}
|
||||
}
|
||||
return rule
|
||||
}
|
||||
|
||||
func names(rules []*ListableRule) []string {
|
||||
out := make([]string, 0, len(rules))
|
||||
for _, rule := range rules {
|
||||
out = append(out, rule.AlertName)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func TestToListableRule(t *testing.T) {
|
||||
created := time.Date(2026, 8, 1, 0, 0, 0, 0, time.UTC)
|
||||
updated := time.Date(2026, 9, 1, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
storable := &StorableRule{
|
||||
Identifiable: types.Identifiable{ID: valuer.GenerateUUID()},
|
||||
TimeAuditable: types.TimeAuditable{CreatedAt: created, UpdatedAt: updated},
|
||||
UserAuditable: types.UserAuditable{CreatedBy: "creator@signoz.io", UpdatedBy: "updater@signoz.io"},
|
||||
Data: `{"alert":"High CPU","description":"cpu is hot","alertType":"METRIC_BASED_ALERT","ruleType":"threshold_rule","disabled":true,"labels":{"severity":"critical"}}`,
|
||||
}
|
||||
|
||||
listable, err := storable.ToListableRule()
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, storable.ID.StringValue(), listable.Id)
|
||||
assert.Equal(t, "High CPU", listable.AlertName)
|
||||
assert.Equal(t, "cpu is hot", listable.Description)
|
||||
assert.Equal(t, AlertTypeMetric, listable.AlertType)
|
||||
assert.Equal(t, RuleTypeThreshold, listable.RuleType)
|
||||
assert.True(t, listable.Disabled)
|
||||
assert.Equal(t, map[string]string{"severity": "critical"}, listable.Labels)
|
||||
assert.Equal(t, created, listable.CreatedAt)
|
||||
assert.Equal(t, "creator@signoz.io", listable.CreatedBy)
|
||||
assert.Equal(t, updated, listable.UpdatedAt)
|
||||
assert.Equal(t, "updater@signoz.io", listable.UpdatedBy)
|
||||
assert.True(t, listable.State.IsZero())
|
||||
|
||||
_, err = (&StorableRule{Data: "not json"}).ToListableRule()
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestNewListableRulesFromStorableRules(t *testing.T) {
|
||||
enabledID := valuer.GenerateUUID()
|
||||
pausedID := valuer.GenerateUUID()
|
||||
corruptID := valuer.GenerateUUID()
|
||||
|
||||
storedRules := []*StorableRule{
|
||||
{
|
||||
Identifiable: types.Identifiable{ID: enabledID},
|
||||
Data: `{"alert":"cpu high","alertType":"METRIC_BASED_ALERT","ruleType":"threshold_rule"}`,
|
||||
},
|
||||
{
|
||||
Identifiable: types.Identifiable{ID: pausedID},
|
||||
Data: `{"alert":"mem high","alertType":"METRIC_BASED_ALERT","ruleType":"threshold_rule","disabled":true}`,
|
||||
},
|
||||
{
|
||||
Identifiable: types.Identifiable{ID: corruptID},
|
||||
Data: "not json",
|
||||
},
|
||||
}
|
||||
stateByRuleID := map[string]AlertState{enabledID.StringValue(): StateFiring}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
stateFilter map[AlertState]struct{}
|
||||
wantNames []string
|
||||
wantStates map[string]AlertState
|
||||
}{
|
||||
{
|
||||
name: "NoFilter_KeepsAllParseableRows",
|
||||
wantNames: []string{"cpu high", "mem high"},
|
||||
wantStates: map[string]AlertState{"cpu high": StateFiring, "mem high": StateDisabled},
|
||||
},
|
||||
{
|
||||
name: "FiringFilter_KeepsOverlaidState",
|
||||
stateFilter: map[AlertState]struct{}{StateFiring: {}},
|
||||
wantNames: []string{"cpu high"},
|
||||
wantStates: map[string]AlertState{"cpu high": StateFiring},
|
||||
},
|
||||
{
|
||||
name: "DisabledFilter_KeepsAbsentFromSnapshot",
|
||||
stateFilter: map[AlertState]struct{}{StateDisabled: {}},
|
||||
wantNames: []string{"mem high"},
|
||||
wantStates: map[string]AlertState{"mem high": StateDisabled},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
listableRules, errByRuleID := NewListableRulesFromStorableRules(storedRules, stateByRuleID, testCase.stateFilter)
|
||||
|
||||
require.Len(t, errByRuleID, 1)
|
||||
assert.Error(t, errByRuleID[corruptID.StringValue()])
|
||||
|
||||
assert.Equal(t, testCase.wantNames, names(listableRules))
|
||||
for _, rule := range listableRules {
|
||||
assert.Equal(t, testCase.wantStates[rule.AlertName], rule.State)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
disabledRow, _ := NewListableRulesFromStorableRules(storedRules[1:2], stateByRuleID, nil)
|
||||
require.Len(t, disabledRow, 1)
|
||||
assert.True(t, disabledRow[0].Disabled)
|
||||
}
|
||||
|
||||
func TestSortListableRules(t *testing.T) {
|
||||
base := time.Date(2026, 9, 1, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
rules []*ListableRule
|
||||
sortBy ListSort
|
||||
order ListOrder
|
||||
wantNames []string
|
||||
}{
|
||||
{
|
||||
name: "state desc is display priority firing first",
|
||||
rules: []*ListableRule{
|
||||
listableRule("disabled", StateDisabled, "", base),
|
||||
listableRule("nodata", StateNoData, "", base),
|
||||
listableRule("firing", StateFiring, "", base),
|
||||
listableRule("inactive", StateInactive, "", base),
|
||||
listableRule("pending", StatePending, "", base),
|
||||
listableRule("recovering", StateRecovering, "", base),
|
||||
},
|
||||
sortBy: ListSortState,
|
||||
order: ListOrderDesc,
|
||||
wantNames: []string{"firing", "nodata", "pending", "recovering", "inactive", "disabled"},
|
||||
},
|
||||
{
|
||||
name: "severity desc ranks known values then custom ones lexically",
|
||||
rules: []*ListableRule{
|
||||
listableRule("warn", StateInactive, "warning", base),
|
||||
listableRule("custom-b", StateInactive, "bbb", base),
|
||||
listableRule("crit", StateInactive, "critical", base),
|
||||
listableRule("custom-a", StateInactive, "aaa", base),
|
||||
listableRule("none", StateInactive, "", base),
|
||||
},
|
||||
sortBy: ListSortSeverity,
|
||||
order: ListOrderDesc,
|
||||
// desc flips the lexical compare between custom values too
|
||||
wantNames: []string{"crit", "warn", "custom-b", "custom-a", "none"},
|
||||
},
|
||||
{
|
||||
name: "name asc is case-insensitive",
|
||||
rules: []*ListableRule{
|
||||
listableRule("banana", StateInactive, "", base),
|
||||
listableRule("Apple", StateInactive, "", base),
|
||||
listableRule("cherry", StateInactive, "", base),
|
||||
},
|
||||
sortBy: ListSortName,
|
||||
order: ListOrderAsc,
|
||||
wantNames: []string{"Apple", "banana", "cherry"},
|
||||
},
|
||||
{
|
||||
name: "updated_at desc puts newest first",
|
||||
rules: []*ListableRule{
|
||||
listableRule("old", StateInactive, "", base),
|
||||
listableRule("new", StateInactive, "", base.Add(time.Hour)),
|
||||
},
|
||||
sortBy: ListSortUpdatedAt,
|
||||
order: ListOrderDesc,
|
||||
wantNames: []string{"new", "old"},
|
||||
},
|
||||
{
|
||||
name: "state desc ties break on name asc",
|
||||
rules: []*ListableRule{
|
||||
listableRule("banana", StateFiring, "", base),
|
||||
listableRule("zebra", StateDisabled, "", base),
|
||||
listableRule("Apple", StateFiring, "", base),
|
||||
listableRule("cherry", StateFiring, "", base),
|
||||
},
|
||||
sortBy: ListSortState,
|
||||
order: ListOrderDesc,
|
||||
wantNames: []string{"Apple", "banana", "cherry", "zebra"},
|
||||
},
|
||||
{
|
||||
name: "state asc flips buckets but tiebreak stays name asc",
|
||||
rules: []*ListableRule{
|
||||
listableRule("banana", StateFiring, "", base),
|
||||
listableRule("zebra", StateDisabled, "", base),
|
||||
listableRule("Apple", StateFiring, "", base),
|
||||
listableRule("cherry", StateFiring, "", base),
|
||||
},
|
||||
sortBy: ListSortState,
|
||||
order: ListOrderAsc,
|
||||
wantNames: []string{"zebra", "Apple", "banana", "cherry"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
SortListableRules(tc.rules, tc.sortBy, tc.order)
|
||||
assert.Equal(t, tc.wantNames, names(tc.rules))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSortListableRulesIdTiebreak(t *testing.T) {
|
||||
base := time.Date(2026, 9, 1, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
for _, order := range []ListOrder{ListOrderAsc, ListOrderDesc} {
|
||||
t.Run(order.StringValue(), func(t *testing.T) {
|
||||
older := listableRule("dup", StateFiring, "", base)
|
||||
older.Id = "01aaa"
|
||||
newer := listableRule("dup", StateFiring, "", base)
|
||||
newer.Id = "01bbb"
|
||||
|
||||
rules := []*ListableRule{newer, older}
|
||||
SortListableRules(rules, ListSortState, order)
|
||||
assert.Equal(t, []string{"01aaa", "01bbb"}, []string{rules[0].Id, rules[1].Id})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewLabelPairsFromRawJSON(t *testing.T) {
|
||||
pairs := NewLabelPairsFromRawJSON([]string{
|
||||
`{"team":"infra","severity":"critical"}`,
|
||||
`{"team":"infra"}`,
|
||||
`{"team":"payments"}`,
|
||||
"",
|
||||
"null",
|
||||
"not-json",
|
||||
}, MaxListLabelPairs)
|
||||
|
||||
assert.Equal(t, []LabelPair{
|
||||
{Key: "severity", Value: "critical"},
|
||||
{Key: "team", Value: "infra"},
|
||||
{Key: "team", Value: "payments"},
|
||||
}, pairs)
|
||||
}
|
||||
|
||||
func TestNewLabelPairsFromRawJSONCap(t *testing.T) {
|
||||
pairs := NewLabelPairsFromRawJSON([]string{`{"a":"1","b":"2","c":"3"}`}, 2)
|
||||
assert.Len(t, pairs, 2)
|
||||
}
|
||||
@@ -11,8 +11,7 @@ import (
|
||||
)
|
||||
|
||||
type StorableRule struct {
|
||||
// The alias must stay rule: the list filter compiler emits rule.<col> refs.
|
||||
bun.BaseModel `bun:"table:rule,alias:rule"`
|
||||
bun.BaseModel `bun:"table:rule"`
|
||||
types.Identifiable
|
||||
types.TimeAuditable
|
||||
types.UserAuditable
|
||||
@@ -59,16 +58,6 @@ type RuleStore interface {
|
||||
EditRule(context.Context, *StorableRule, func(context.Context) error) error
|
||||
DeleteRule(context.Context, valuer.UUID, valuer.UUID, func(context.Context) error) error
|
||||
GetStoredRules(context.Context, string) ([]*StorableRule, error)
|
||||
// GetStoredRulesMatching returns the org's rules matching a list filter query; an empty query matches all.
|
||||
GetStoredRulesMatching(context.Context, string, string) ([]*StorableRule, error)
|
||||
// GetStoredRuleLabels returns each rule's labels as raw JSON text, empty string when absent.
|
||||
GetStoredRuleLabels(context.Context, string) ([]string, error)
|
||||
GetStoredRule(context.Context, valuer.UUID, valuer.UUID) (*StorableRule, error)
|
||||
GetStoredRulesByMetricName(context.Context, string, string) ([]RuleAlert, error)
|
||||
|
||||
CreateRuleView(context.Context, *RuleView) error
|
||||
GetRuleView(context.Context, valuer.UUID, valuer.UUID) (*RuleView, error)
|
||||
ListRuleViews(context.Context, valuer.UUID) ([]*RuleView, error)
|
||||
UpdateRuleView(context.Context, *RuleView) error
|
||||
DeleteRuleView(context.Context, valuer.UUID, valuer.UUID) error
|
||||
}
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
package ruletypes
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
const (
|
||||
RuleViewSchemaVersion = "v1"
|
||||
MaxRuleViewNameLen = 64
|
||||
)
|
||||
|
||||
var (
|
||||
ErrCodeRuleViewInvalidInput = errors.MustNewCode("rule_view_invalid_input")
|
||||
ErrCodeRuleViewNotFound = errors.MustNewCode("rule_view_not_found")
|
||||
)
|
||||
|
||||
type RuleView struct {
|
||||
bun.BaseModel `bun:"table:rule_view,alias:rule_view"`
|
||||
|
||||
types.Identifiable
|
||||
types.TimeAuditable
|
||||
|
||||
Name string `bun:"name,type:text,notnull" json:"name" required:"true"`
|
||||
Data RuleViewData `bun:"data,type:text,notnull" json:"data" required:"true"`
|
||||
OrgID valuer.UUID `bun:"org_id,type:text,notnull" json:"orgId" required:"true"`
|
||||
}
|
||||
|
||||
// RuleViewData holds the rule listing state (ListRulesParams minus pagination) a view replays.
|
||||
type RuleViewData struct {
|
||||
Version string `json:"version" required:"true"`
|
||||
ListFilter
|
||||
}
|
||||
|
||||
func (d *RuleViewData) Validate() error {
|
||||
if d.Version != RuleViewSchemaVersion {
|
||||
return errors.NewInvalidInputf(ErrCodeRuleViewInvalidInput,
|
||||
"version must be %q, got %q", RuleViewSchemaVersion, d.Version)
|
||||
}
|
||||
return d.ListFilter.Validate()
|
||||
}
|
||||
|
||||
type PostableRuleView struct {
|
||||
Name string `json:"name" required:"true"`
|
||||
Data RuleViewData `json:"data" required:"true"`
|
||||
}
|
||||
|
||||
func (p *PostableRuleView) UnmarshalJSON(data []byte) error {
|
||||
dec := json.NewDecoder(bytes.NewReader(data))
|
||||
dec.DisallowUnknownFields()
|
||||
type alias PostableRuleView
|
||||
var tmp alias
|
||||
if err := dec.Decode(&tmp); err != nil {
|
||||
return errors.WrapInvalidInputf(err, ErrCodeRuleViewInvalidInput, "invalid saved view request body").WithAdditional(err.Error())
|
||||
}
|
||||
*p = PostableRuleView(tmp)
|
||||
return p.Validate()
|
||||
}
|
||||
|
||||
func (p *PostableRuleView) Validate() error {
|
||||
if err := validateRuleViewName(p.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
return p.Data.Validate()
|
||||
}
|
||||
|
||||
func (p PostableRuleView) NewRuleView(orgID valuer.UUID) *RuleView {
|
||||
now := time.Now()
|
||||
return &RuleView{
|
||||
Identifiable: types.Identifiable{ID: valuer.GenerateUUID()},
|
||||
TimeAuditable: types.TimeAuditable{CreatedAt: now, UpdatedAt: now},
|
||||
OrgID: orgID,
|
||||
Name: p.Name,
|
||||
Data: p.Data,
|
||||
}
|
||||
}
|
||||
|
||||
type UpdatableRuleView = PostableRuleView
|
||||
|
||||
func (v *RuleView) Update(updateable UpdatableRuleView) {
|
||||
v.Name = updateable.Name
|
||||
v.Data = updateable.Data
|
||||
v.UpdatedAt = time.Now()
|
||||
}
|
||||
|
||||
type ListableRuleViews struct {
|
||||
Views []*RuleView `json:"views" required:"true" nullable:"false"`
|
||||
}
|
||||
|
||||
func validateRuleViewName(name string) error {
|
||||
if strings.TrimSpace(name) == "" {
|
||||
return errors.NewInvalidInputf(ErrCodeRuleViewInvalidInput, "name is required")
|
||||
}
|
||||
if name != strings.TrimSpace(name) {
|
||||
return errors.NewInvalidInputf(ErrCodeRuleViewInvalidInput, "name must not have leading or trailing whitespace")
|
||||
}
|
||||
if n := utf8.RuneCountInString(name); n > MaxRuleViewNameLen {
|
||||
return errors.NewInvalidInputf(ErrCodeRuleViewInvalidInput,
|
||||
"name must be at most %d characters, got %d", MaxRuleViewNameLen, n)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,184 +0,0 @@
|
||||
package ruletypes
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRuleViewDataValidate(t *testing.T) {
|
||||
cases := []struct {
|
||||
description string
|
||||
data RuleViewData
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
description: "valid with all fields set",
|
||||
data: RuleViewData{Version: RuleViewSchemaVersion, ListFilter: ListFilter{Query: "name CONTAINS 'prod'", States: []string{"firing", "pending"}, Sort: ListSortName, Order: ListOrderAsc}},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
description: "valid with zero states, sort and order",
|
||||
data: RuleViewData{Version: RuleViewSchemaVersion},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
description: "query over the cap is rejected",
|
||||
data: RuleViewData{Version: RuleViewSchemaVersion, ListFilter: ListFilter{Query: strings.Repeat("x", MaxListQueryLen+1)}},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
description: "wrong version is rejected",
|
||||
data: RuleViewData{Version: "v2"},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
description: "empty version is rejected",
|
||||
data: RuleViewData{},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
description: "unknown state is rejected",
|
||||
data: RuleViewData{Version: RuleViewSchemaVersion, ListFilter: ListFilter{States: []string{"exploding"}}},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
description: "unknown sort is rejected",
|
||||
data: RuleViewData{Version: RuleViewSchemaVersion, ListFilter: ListFilter{Sort: ListSort{valuer.NewString("bogus")}}},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
description: "unknown order is rejected",
|
||||
data: RuleViewData{Version: RuleViewSchemaVersion, ListFilter: ListFilter{Order: ListOrder{valuer.NewString("sideways")}}},
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.description, func(t *testing.T) {
|
||||
err := c.data.Validate()
|
||||
if c.expectError {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRuleViewDataValidateDefaults(t *testing.T) {
|
||||
data := RuleViewData{Version: RuleViewSchemaVersion}
|
||||
require.NoError(t, data.Validate())
|
||||
assert.Equal(t, ListSortUpdatedAt, data.Sort)
|
||||
assert.Equal(t, ListOrderDesc, data.Order)
|
||||
assert.Equal(t, []string{}, data.States)
|
||||
}
|
||||
|
||||
func TestPostableRuleViewUnmarshalJSON(t *testing.T) {
|
||||
cases := []struct {
|
||||
description string
|
||||
body string
|
||||
expectError bool
|
||||
expectedErrMsg string
|
||||
expectedName string
|
||||
}{
|
||||
{
|
||||
description: "valid body keeps name as-is",
|
||||
body: `{"name":"my view","data":{"version":"v1","query":"severity = 'critical'","states":["firing"],"sort":"name","order":"asc"}}`,
|
||||
expectError: false,
|
||||
expectedName: "my view",
|
||||
},
|
||||
{
|
||||
description: "name with surrounding whitespace is rejected",
|
||||
body: `{"name":" my view ","data":{"version":"v1"}}`,
|
||||
expectError: true,
|
||||
expectedErrMsg: "name must not have leading or trailing whitespace",
|
||||
},
|
||||
{
|
||||
description: "unknown field is rejected",
|
||||
body: `{"name":"my view","data":{"version":"v1"},"extra":true}`,
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
description: "blank name is rejected",
|
||||
body: `{"name":" ","data":{"version":"v1"}}`,
|
||||
expectError: true,
|
||||
expectedErrMsg: "name is required",
|
||||
},
|
||||
{
|
||||
description: "name over max length is rejected",
|
||||
body: `{"name":"` + strings.Repeat("x", MaxRuleViewNameLen+1) + `","data":{"version":"v1"}}`,
|
||||
expectError: true,
|
||||
expectedErrMsg: "name must be at most",
|
||||
},
|
||||
{
|
||||
description: "invalid data version is rejected",
|
||||
body: `{"name":"my view","data":{"version":"v9"}}`,
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
description: "invalid state is rejected",
|
||||
body: `{"name":"my view","data":{"version":"v1","states":["exploding"]}}`,
|
||||
expectError: true,
|
||||
expectedErrMsg: "invalid state",
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.description, func(t *testing.T) {
|
||||
var p PostableRuleView
|
||||
err := json.Unmarshal([]byte(c.body), &p)
|
||||
if c.expectError {
|
||||
assert.Error(t, err)
|
||||
if c.expectedErrMsg != "" {
|
||||
assert.ErrorContains(t, err, c.expectedErrMsg)
|
||||
}
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, c.expectedName, p.Name)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPostableRuleViewNewRuleView(t *testing.T) {
|
||||
orgID := valuer.GenerateUUID()
|
||||
postable := PostableRuleView{
|
||||
Name: "my view",
|
||||
Data: RuleViewData{Version: RuleViewSchemaVersion, ListFilter: ListFilter{States: []string{"firing"}, Sort: ListSortName, Order: ListOrderAsc}},
|
||||
}
|
||||
|
||||
view := postable.NewRuleView(orgID)
|
||||
|
||||
assert.Equal(t, orgID, view.OrgID)
|
||||
assert.Equal(t, "my view", view.Name)
|
||||
assert.Equal(t, postable.Data, view.Data)
|
||||
assert.False(t, view.ID.IsZero())
|
||||
assert.False(t, view.CreatedAt.IsZero())
|
||||
assert.Equal(t, view.CreatedAt, view.UpdatedAt)
|
||||
}
|
||||
|
||||
func TestRuleViewUpdate(t *testing.T) {
|
||||
orgID := valuer.GenerateUUID()
|
||||
view := PostableRuleView{
|
||||
Name: "original",
|
||||
Data: RuleViewData{Version: RuleViewSchemaVersion, ListFilter: ListFilter{Sort: ListSortName, Order: ListOrderAsc}},
|
||||
}.NewRuleView(orgID)
|
||||
createdAt := view.CreatedAt
|
||||
|
||||
view.Update(UpdatableRuleView{
|
||||
Name: "renamed",
|
||||
Data: RuleViewData{Version: RuleViewSchemaVersion, ListFilter: ListFilter{States: []string{"disabled"}, Sort: ListSortCreatedAt, Order: ListOrderDesc}},
|
||||
})
|
||||
|
||||
assert.Equal(t, "renamed", view.Name)
|
||||
assert.Equal(t, []string{"disabled"}, view.Data.States)
|
||||
assert.Equal(t, ListSortCreatedAt, view.Data.Sort)
|
||||
assert.Equal(t, ListOrderDesc, view.Data.Order)
|
||||
assert.Equal(t, createdAt, view.CreatedAt)
|
||||
assert.True(t, view.UpdatedAt.After(createdAt))
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user