mirror of
https://github.com/SigNoz/signoz.git
synced 2026-08-31 16:40:44 +01:00
Compare commits
3 Commits
issue_4501
...
promql-res
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
813b5de8de | ||
|
|
bb7b00f292 | ||
|
|
abebea532b |
1
.github/workflows/integrationci.yaml
vendored
1
.github/workflows/integrationci.yaml
vendored
@@ -58,6 +58,7 @@ jobs:
|
||||
- querierai
|
||||
- rawexportdata
|
||||
- promqlconformance
|
||||
- promapiconformance
|
||||
- querierauthz
|
||||
- role
|
||||
- rootuser
|
||||
|
||||
@@ -46,7 +46,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrystore"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/SigNoz/signoz/pkg/version"
|
||||
"github.com/SigNoz/signoz/pkg/zeus"
|
||||
@@ -104,8 +103,8 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
|
||||
|
||||
return openfgaauthz.NewProviderFactory(sqlstore, openfgaschema.NewSchema().Get(ctx), openfgaDataStore, authtypes.NewRegistry()), nil
|
||||
},
|
||||
func(store sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, _ querier.Querier, _ licensing.Licensing, tagModule tag.Module, systemDashboardRegistry dashboardtypes.SystemDashboardRegistry) dashboard.Module {
|
||||
return impldashboard.NewModule(impldashboard.NewStore(store), settings, analytics, orgGetter, queryParser, tagModule, systemDashboardRegistry)
|
||||
func(store sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, _ querier.Querier, _ licensing.Licensing, tagModule tag.Module) dashboard.Module {
|
||||
return impldashboard.NewModule(impldashboard.NewStore(store), settings, analytics, orgGetter, queryParser, tagModule)
|
||||
},
|
||||
func(_ licensing.Licensing) factory.ProviderFactory[gateway.Gateway, gateway.Config] {
|
||||
return noopgateway.NewProviderFactory()
|
||||
|
||||
@@ -63,7 +63,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/telemetrystore"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/SigNoz/signoz/pkg/version"
|
||||
"github.com/SigNoz/signoz/pkg/zeus"
|
||||
@@ -137,8 +136,8 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
|
||||
}
|
||||
return openfgaauthz.NewProviderFactory(sqlstore, openfgaschema.NewSchema().Get(ctx), openfgaDataStore, licensing, onBeforeRoleDelete, authtypes.NewRegistry()), nil
|
||||
},
|
||||
func(store sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, querier querier.Querier, licensing licensing.Licensing, tagModule tag.Module, systemDashboardRegistry dashboardtypes.SystemDashboardRegistry) dashboard.Module {
|
||||
return impldashboard.NewModule(pkgimpldashboard.NewStore(store), settings, analytics, orgGetter, queryParser, querier, licensing, tagModule, systemDashboardRegistry)
|
||||
func(store sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, querier querier.Querier, licensing licensing.Licensing, tagModule tag.Module) dashboard.Module {
|
||||
return impldashboard.NewModule(pkgimpldashboard.NewStore(store), settings, analytics, orgGetter, queryParser, querier, licensing, tagModule)
|
||||
},
|
||||
func(licensing licensing.Licensing) factory.ProviderFactory[gateway.Gateway, gateway.Config] {
|
||||
return httpgateway.NewProviderFactory(licensing)
|
||||
|
||||
@@ -2944,46 +2944,6 @@ components:
|
||||
publicDashboard:
|
||||
$ref: '#/components/schemas/DashboardtypesGettablePublicDasbhboard'
|
||||
type: object
|
||||
DashboardtypesGettableSystemDashboard:
|
||||
properties:
|
||||
createdAt:
|
||||
format: date-time
|
||||
type: string
|
||||
createdBy:
|
||||
type: string
|
||||
image:
|
||||
type: string
|
||||
locked:
|
||||
type: boolean
|
||||
name:
|
||||
type: string
|
||||
orgId:
|
||||
type: string
|
||||
schemaVersion:
|
||||
type: string
|
||||
source:
|
||||
$ref: '#/components/schemas/DashboardtypesSource'
|
||||
spec:
|
||||
$ref: '#/components/schemas/DashboardtypesDashboardSpec'
|
||||
tags:
|
||||
items:
|
||||
$ref: '#/components/schemas/TagtypesGettableTag'
|
||||
nullable: true
|
||||
type: array
|
||||
updatedAt:
|
||||
format: date-time
|
||||
type: string
|
||||
updatedBy:
|
||||
type: string
|
||||
required:
|
||||
- orgId
|
||||
- locked
|
||||
- source
|
||||
- schemaVersion
|
||||
- name
|
||||
- tags
|
||||
- spec
|
||||
type: object
|
||||
DashboardtypesHistogramBuckets:
|
||||
properties:
|
||||
bucketCount:
|
||||
@@ -6500,6 +6460,148 @@ components:
|
||||
type: object
|
||||
PreferencetypesValue:
|
||||
type: object
|
||||
PrometheusErrorResponseSchema:
|
||||
properties:
|
||||
error:
|
||||
type: string
|
||||
errorType:
|
||||
enum:
|
||||
- bad_data
|
||||
- execution
|
||||
- canceled
|
||||
- timeout
|
||||
- internal
|
||||
type: string
|
||||
status:
|
||||
enum:
|
||||
- error
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- errorType
|
||||
- error
|
||||
type: object
|
||||
PrometheusMatrixDataSchema:
|
||||
properties:
|
||||
result:
|
||||
items:
|
||||
$ref: '#/components/schemas/PrometheusMatrixSeriesSchema'
|
||||
nullable: true
|
||||
type: array
|
||||
resultType:
|
||||
enum:
|
||||
- matrix
|
||||
type: string
|
||||
required:
|
||||
- resultType
|
||||
- result
|
||||
type: object
|
||||
PrometheusMatrixSeriesSchema:
|
||||
properties:
|
||||
metric:
|
||||
additionalProperties:
|
||||
type: string
|
||||
nullable: true
|
||||
type: object
|
||||
values:
|
||||
items:
|
||||
$ref: '#/components/schemas/PrometheusSamplePairSchema'
|
||||
nullable: true
|
||||
type: array
|
||||
required:
|
||||
- metric
|
||||
- values
|
||||
type: object
|
||||
PrometheusQueryDataSchema:
|
||||
oneOf:
|
||||
- $ref: '#/components/schemas/PrometheusMatrixDataSchema'
|
||||
- $ref: '#/components/schemas/PrometheusVectorDataSchema'
|
||||
- $ref: '#/components/schemas/PrometheusScalarDataSchema'
|
||||
- $ref: '#/components/schemas/PrometheusStringDataSchema'
|
||||
type: object
|
||||
PrometheusSamplePairSchema:
|
||||
description: 'A [timestamp, value] pair: float unix seconds, then the string-encoded
|
||||
sample value ("NaN", "+Inf", "-Inf" included).'
|
||||
items:
|
||||
oneOf:
|
||||
- type: number
|
||||
- type: string
|
||||
maxItems: 2
|
||||
minItems: 2
|
||||
nullable: true
|
||||
type: array
|
||||
PrometheusScalarDataSchema:
|
||||
properties:
|
||||
result:
|
||||
$ref: '#/components/schemas/PrometheusSamplePairSchema'
|
||||
resultType:
|
||||
enum:
|
||||
- scalar
|
||||
type: string
|
||||
required:
|
||||
- resultType
|
||||
- result
|
||||
type: object
|
||||
PrometheusStringDataSchema:
|
||||
properties:
|
||||
result:
|
||||
$ref: '#/components/schemas/PrometheusSamplePairSchema'
|
||||
resultType:
|
||||
enum:
|
||||
- string
|
||||
type: string
|
||||
required:
|
||||
- resultType
|
||||
- result
|
||||
type: object
|
||||
PrometheusSuccessResponseSchema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/PrometheusQueryDataSchema'
|
||||
infos:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
status:
|
||||
enum:
|
||||
- success
|
||||
type: string
|
||||
warnings:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
PrometheusVectorDataSchema:
|
||||
properties:
|
||||
result:
|
||||
items:
|
||||
$ref: '#/components/schemas/PrometheusVectorSampleSchema'
|
||||
nullable: true
|
||||
type: array
|
||||
resultType:
|
||||
enum:
|
||||
- vector
|
||||
type: string
|
||||
required:
|
||||
- resultType
|
||||
- result
|
||||
type: object
|
||||
PrometheusVectorSampleSchema:
|
||||
properties:
|
||||
metric:
|
||||
additionalProperties:
|
||||
type: string
|
||||
nullable: true
|
||||
type: object
|
||||
value:
|
||||
$ref: '#/components/schemas/PrometheusSamplePairSchema'
|
||||
required:
|
||||
- metric
|
||||
- value
|
||||
type: object
|
||||
PromotetypesPromotePath:
|
||||
properties:
|
||||
indexes:
|
||||
@@ -15399,73 +15501,6 @@ paths:
|
||||
summary: Migrate dashboard to v2
|
||||
tags:
|
||||
- dashboard
|
||||
/api/v2/dashboards/system/{name}:
|
||||
get:
|
||||
deprecated: false
|
||||
description: Returns a dashboard SigNoz ships and owns, addressed by its stable
|
||||
definition name (e.g. `ai-o11y-overview`) rather than its id. System dashboards
|
||||
are read-only and upgraded through releases. The dashboard's own `name` field
|
||||
carries a reserved prefix that the path segment must not include.
|
||||
operationId: GetSystemDashboard
|
||||
parameters:
|
||||
- in: path
|
||||
name: name
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/DashboardtypesGettableSystemDashboard'
|
||||
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:
|
||||
- dashboard:read
|
||||
- tokenizer:
|
||||
- dashboard:read
|
||||
summary: Get system dashboard
|
||||
tags:
|
||||
- dashboard
|
||||
/api/v2/factor_password/forgot:
|
||||
post:
|
||||
deprecated: false
|
||||
@@ -24918,6 +24953,374 @@ paths:
|
||||
summary: Replace variables
|
||||
tags:
|
||||
- querier
|
||||
/prometheus/api/v1/query:
|
||||
get:
|
||||
description: 'Prometheus-compatible endpoint: the request and response contract
|
||||
is the upstream Prometheus HTTP API (https://prometheus.io/docs/prometheus/latest/querying/api/).
|
||||
Parameters are accepted as URL query parameters or a form-encoded body, on
|
||||
GET and POST alike.'
|
||||
operationId: PrometheusQuery
|
||||
parameters:
|
||||
- description: PromQL expression.
|
||||
in: query
|
||||
name: query
|
||||
required: true
|
||||
schema:
|
||||
description: PromQL expression.
|
||||
type: string
|
||||
- description: 'Evaluation timestamp: RFC3339 or float unix seconds. Defaults
|
||||
to the server''s current time.'
|
||||
in: query
|
||||
name: time
|
||||
schema:
|
||||
description: 'Evaluation timestamp: RFC3339 or float unix seconds. Defaults
|
||||
to the server''s current time.'
|
||||
type: string
|
||||
- description: 'Evaluation timeout: duration string or float seconds.'
|
||||
in: query
|
||||
name: timeout
|
||||
schema:
|
||||
description: 'Evaluation timeout: duration string or float seconds.'
|
||||
type: string
|
||||
- description: Any non-empty value includes query statistics in the response.
|
||||
in: query
|
||||
name: stats
|
||||
schema:
|
||||
description: Any non-empty value includes query statistics in the response.
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PrometheusSuccessResponseSchema'
|
||||
description: OK
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PrometheusErrorResponseSchema'
|
||||
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
|
||||
"422":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PrometheusErrorResponseSchema'
|
||||
description: Unprocessable Entity
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PrometheusErrorResponseSchema'
|
||||
description: Internal Server Error
|
||||
"503":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PrometheusErrorResponseSchema'
|
||||
description: Service Unavailable
|
||||
security:
|
||||
- api_key:
|
||||
- metrics:read
|
||||
- tokenizer:
|
||||
- metrics:read
|
||||
summary: Prometheus instant query
|
||||
tags:
|
||||
- prometheus
|
||||
post:
|
||||
description: 'Prometheus-compatible endpoint: the request and response contract
|
||||
is the upstream Prometheus HTTP API (https://prometheus.io/docs/prometheus/latest/querying/api/).
|
||||
Parameters are accepted as URL query parameters or a form-encoded body, on
|
||||
GET and POST alike.'
|
||||
operationId: PrometheusQueryPost
|
||||
parameters:
|
||||
- description: PromQL expression.
|
||||
in: query
|
||||
name: query
|
||||
required: true
|
||||
schema:
|
||||
description: PromQL expression.
|
||||
type: string
|
||||
- description: 'Evaluation timestamp: RFC3339 or float unix seconds. Defaults
|
||||
to the server''s current time.'
|
||||
in: query
|
||||
name: time
|
||||
schema:
|
||||
description: 'Evaluation timestamp: RFC3339 or float unix seconds. Defaults
|
||||
to the server''s current time.'
|
||||
type: string
|
||||
- description: 'Evaluation timeout: duration string or float seconds.'
|
||||
in: query
|
||||
name: timeout
|
||||
schema:
|
||||
description: 'Evaluation timeout: duration string or float seconds.'
|
||||
type: string
|
||||
- description: Any non-empty value includes query statistics in the response.
|
||||
in: query
|
||||
name: stats
|
||||
schema:
|
||||
description: Any non-empty value includes query statistics in the response.
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PrometheusSuccessResponseSchema'
|
||||
description: OK
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PrometheusErrorResponseSchema'
|
||||
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
|
||||
"422":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PrometheusErrorResponseSchema'
|
||||
description: Unprocessable Entity
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PrometheusErrorResponseSchema'
|
||||
description: Internal Server Error
|
||||
"503":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PrometheusErrorResponseSchema'
|
||||
description: Service Unavailable
|
||||
security:
|
||||
- api_key:
|
||||
- metrics:read
|
||||
- tokenizer:
|
||||
- metrics:read
|
||||
summary: Prometheus instant query
|
||||
tags:
|
||||
- prometheus
|
||||
/prometheus/api/v1/query_range:
|
||||
get:
|
||||
description: 'Prometheus-compatible endpoint: the request and response contract
|
||||
is the upstream Prometheus HTTP API (https://prometheus.io/docs/prometheus/latest/querying/api/).
|
||||
Parameters are accepted as URL query parameters or a form-encoded body, on
|
||||
GET and POST alike.'
|
||||
operationId: PrometheusQueryRange
|
||||
parameters:
|
||||
- description: PromQL expression.
|
||||
in: query
|
||||
name: query
|
||||
required: true
|
||||
schema:
|
||||
description: PromQL expression.
|
||||
type: string
|
||||
- description: 'Range start: RFC3339 or float unix seconds.'
|
||||
in: query
|
||||
name: start
|
||||
required: true
|
||||
schema:
|
||||
description: 'Range start: RFC3339 or float unix seconds.'
|
||||
type: string
|
||||
- description: 'Range end: RFC3339 or float unix seconds.'
|
||||
in: query
|
||||
name: end
|
||||
required: true
|
||||
schema:
|
||||
description: 'Range end: RFC3339 or float unix seconds.'
|
||||
type: string
|
||||
- description: 'Resolution step: duration string or float seconds.'
|
||||
in: query
|
||||
name: step
|
||||
required: true
|
||||
schema:
|
||||
description: 'Resolution step: duration string or float seconds.'
|
||||
type: string
|
||||
- description: 'Evaluation timeout: duration string or float seconds.'
|
||||
in: query
|
||||
name: timeout
|
||||
schema:
|
||||
description: 'Evaluation timeout: duration string or float seconds.'
|
||||
type: string
|
||||
- description: Any non-empty value includes query statistics in the response.
|
||||
in: query
|
||||
name: stats
|
||||
schema:
|
||||
description: Any non-empty value includes query statistics in the response.
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PrometheusSuccessResponseSchema'
|
||||
description: OK
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PrometheusErrorResponseSchema'
|
||||
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
|
||||
"422":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PrometheusErrorResponseSchema'
|
||||
description: Unprocessable Entity
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PrometheusErrorResponseSchema'
|
||||
description: Internal Server Error
|
||||
"503":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PrometheusErrorResponseSchema'
|
||||
description: Service Unavailable
|
||||
security:
|
||||
- api_key:
|
||||
- metrics:read
|
||||
- tokenizer:
|
||||
- metrics:read
|
||||
summary: Prometheus range query
|
||||
tags:
|
||||
- prometheus
|
||||
post:
|
||||
description: 'Prometheus-compatible endpoint: the request and response contract
|
||||
is the upstream Prometheus HTTP API (https://prometheus.io/docs/prometheus/latest/querying/api/).
|
||||
Parameters are accepted as URL query parameters or a form-encoded body, on
|
||||
GET and POST alike.'
|
||||
operationId: PrometheusQueryRangePost
|
||||
parameters:
|
||||
- description: PromQL expression.
|
||||
in: query
|
||||
name: query
|
||||
required: true
|
||||
schema:
|
||||
description: PromQL expression.
|
||||
type: string
|
||||
- description: 'Range start: RFC3339 or float unix seconds.'
|
||||
in: query
|
||||
name: start
|
||||
required: true
|
||||
schema:
|
||||
description: 'Range start: RFC3339 or float unix seconds.'
|
||||
type: string
|
||||
- description: 'Range end: RFC3339 or float unix seconds.'
|
||||
in: query
|
||||
name: end
|
||||
required: true
|
||||
schema:
|
||||
description: 'Range end: RFC3339 or float unix seconds.'
|
||||
type: string
|
||||
- description: 'Resolution step: duration string or float seconds.'
|
||||
in: query
|
||||
name: step
|
||||
required: true
|
||||
schema:
|
||||
description: 'Resolution step: duration string or float seconds.'
|
||||
type: string
|
||||
- description: 'Evaluation timeout: duration string or float seconds.'
|
||||
in: query
|
||||
name: timeout
|
||||
schema:
|
||||
description: 'Evaluation timeout: duration string or float seconds.'
|
||||
type: string
|
||||
- description: Any non-empty value includes query statistics in the response.
|
||||
in: query
|
||||
name: stats
|
||||
schema:
|
||||
description: Any non-empty value includes query statistics in the response.
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PrometheusSuccessResponseSchema'
|
||||
description: OK
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PrometheusErrorResponseSchema'
|
||||
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
|
||||
"422":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PrometheusErrorResponseSchema'
|
||||
description: Unprocessable Entity
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PrometheusErrorResponseSchema'
|
||||
description: Internal Server Error
|
||||
"503":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PrometheusErrorResponseSchema'
|
||||
description: Service Unavailable
|
||||
security:
|
||||
- api_key:
|
||||
- metrics:read
|
||||
- tokenizer:
|
||||
- metrics:read
|
||||
summary: Prometheus range query
|
||||
tags:
|
||||
- prometheus
|
||||
servers:
|
||||
- description: The fully qualified URL to the SigNoz APIServer.
|
||||
url: https://{host}:{port}{base_path}
|
||||
|
||||
@@ -299,8 +299,21 @@ substituted. One subtlety makes it exact: we write stale markers at absent
|
||||
grid points. Without them, the engine's lookback would resurrect a point
|
||||
from up to `lookback` earlier. The marker encodes "absent here" the way the
|
||||
engine itself encodes it. Units evaluate concurrently. Each unit is one
|
||||
series lookup plus one grid statement. A step of 0 is an instant query: a
|
||||
single evaluation at `end`.
|
||||
grid statement: the group-key join resolves the matchers, and the samples
|
||||
primary key takes the metric name straight from the selector. Only a
|
||||
selector without a static `__name__` runs the series lookup first, to learn
|
||||
the concrete metric names. A step of 0 is an instant query: a single
|
||||
evaluation at `end`.
|
||||
|
||||
Both paths enforce fetch budgets
|
||||
(`prometheus::clickhousev2::max_fetched_series` and
|
||||
`::max_fetched_samples`; 0 disables). The engine path counts matched series
|
||||
and scanned samples. The transpiled path counts buffered grid cells (series
|
||||
times grid width) across a plan's units, because transpiled results never
|
||||
pass the engine's sample limiter. A refusal is a typed invalid-input error.
|
||||
It pierces the engine's `promql.ErrStorage` wrapper
|
||||
(`prometheus.TypedStorageError`), so the APIs report a user error, not an
|
||||
internal one.
|
||||
|
||||
A note on the window sliver: when the window is narrower than the step, the
|
||||
grid windows cover only `window/step` of the timeline. A sample in a gap
|
||||
@@ -315,8 +328,9 @@ selectors and `last_over_time` transpile at window < step too.
|
||||
|
||||
## Series lookup
|
||||
|
||||
Both paths resolve matchers the same way, once per selector
|
||||
(`selectSeries`). The series tables hold one row per (fingerprint, bucket)
|
||||
The engine path resolves matchers once per selector (`selectSeries`); the
|
||||
transpiled path builds the same conditions into its group-key join. Both
|
||||
read the same tables. The series tables hold one row per (fingerprint, bucket)
|
||||
at 1h/6h/1d/1w granularities. The shared schema package
|
||||
(`pkg/telemetryschema/metricstelemetryschema`) picks the table whose bucket
|
||||
fits the window. It rounds the window start down to the bucket boundary, so
|
||||
|
||||
@@ -32,9 +32,9 @@ type module struct {
|
||||
tagModule tag.Module
|
||||
}
|
||||
|
||||
func NewModule(store dashboardtypes.Store, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, querier querier.Querier, licensing licensing.Licensing, tagModule tag.Module, systemDashboardRegistry dashboardtypes.SystemDashboardRegistry) dashboard.Module {
|
||||
func NewModule(store dashboardtypes.Store, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, querier querier.Querier, licensing licensing.Licensing, tagModule tag.Module) dashboard.Module {
|
||||
scopedProviderSettings := factory.NewScopedProviderSettings(settings, "github.com/SigNoz/signoz/ee/modules/dashboard/impldashboard")
|
||||
pkgDashboardModule := pkgimpldashboard.NewModule(store, settings, analytics, orgGetter, queryParser, tagModule, systemDashboardRegistry)
|
||||
pkgDashboardModule := pkgimpldashboard.NewModule(store, settings, analytics, orgGetter, queryParser, tagModule)
|
||||
|
||||
return &module{
|
||||
pkgDashboardModule: pkgDashboardModule,
|
||||
@@ -361,14 +361,6 @@ func (module *module) LockUnlock(ctx context.Context, orgID valuer.UUID, id valu
|
||||
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)
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
@@ -46,8 +46,6 @@ import type {
|
||||
GetPublicDashboardPathParameters,
|
||||
GetPublicDashboardWidgetQueryRange200,
|
||||
GetPublicDashboardWidgetQueryRangePathParameters,
|
||||
GetSystemDashboard200,
|
||||
GetSystemDashboardPathParameters,
|
||||
ListDashboardViews200,
|
||||
ListDashboardsForUserV2200,
|
||||
ListDashboardsForUserV2Params,
|
||||
@@ -1887,108 +1885,6 @@ export const useMigrateDashboardV2 = <
|
||||
> => {
|
||||
return useMutation(getMigrateDashboardV2MutationOptions(options));
|
||||
};
|
||||
/**
|
||||
* Returns a dashboard SigNoz ships and owns, addressed by its stable definition name (e.g. `ai-o11y-overview`) rather than its id. System dashboards are read-only and upgraded through releases. The dashboard's own `name` field carries a reserved prefix that the path segment must not include.
|
||||
* @summary Get system dashboard
|
||||
*/
|
||||
export const getSystemDashboard = (
|
||||
{ name }: GetSystemDashboardPathParameters,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<GetSystemDashboard200>({
|
||||
url: `/api/v2/dashboards/system/${name}`,
|
||||
method: 'GET',
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getGetSystemDashboardQueryKey = ({
|
||||
name,
|
||||
}: GetSystemDashboardPathParameters) => {
|
||||
return [`/api/v2/dashboards/system/${name}`] as const;
|
||||
};
|
||||
|
||||
export const getGetSystemDashboardQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof getSystemDashboard>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(
|
||||
{ name }: GetSystemDashboardPathParameters,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getSystemDashboard>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
) => {
|
||||
const { query: queryOptions } = options ?? {};
|
||||
|
||||
const queryKey =
|
||||
queryOptions?.queryKey ?? getGetSystemDashboardQueryKey({ name });
|
||||
|
||||
const queryFn: QueryFunction<
|
||||
Awaited<ReturnType<typeof getSystemDashboard>>
|
||||
> = ({ signal }) => getSystemDashboard({ name }, signal);
|
||||
|
||||
return {
|
||||
queryKey,
|
||||
queryFn,
|
||||
enabled: !!name,
|
||||
...queryOptions,
|
||||
} as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getSystemDashboard>>,
|
||||
TError,
|
||||
TData
|
||||
> & { queryKey: QueryKey };
|
||||
};
|
||||
|
||||
export type GetSystemDashboardQueryResult = NonNullable<
|
||||
Awaited<ReturnType<typeof getSystemDashboard>>
|
||||
>;
|
||||
export type GetSystemDashboardQueryError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Get system dashboard
|
||||
*/
|
||||
|
||||
export function useGetSystemDashboard<
|
||||
TData = Awaited<ReturnType<typeof getSystemDashboard>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(
|
||||
{ name }: GetSystemDashboardPathParameters,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getSystemDashboard>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getGetSystemDashboardQueryOptions({ name }, options);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
};
|
||||
|
||||
return { ...query, queryKey: queryOptions.queryKey };
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get system dashboard
|
||||
*/
|
||||
export const invalidateGetSystemDashboard = async (
|
||||
queryClient: QueryClient,
|
||||
{ name }: GetSystemDashboardPathParameters,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
{ queryKey: getGetSystemDashboardQueryKey({ name }) },
|
||||
options,
|
||||
);
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
/**
|
||||
* This endpoint returns the sanitized v2-shape dashboard data for public access. Each panel query is reduced to a safe field subset, so filters and raw query strings are not exposed.
|
||||
* @summary Get public dashboard data (v2)
|
||||
|
||||
396
frontend/src/api/generated/services/prometheus/index.ts
Normal file
396
frontend/src/api/generated/services/prometheus/index.ts
Normal file
@@ -0,0 +1,396 @@
|
||||
/**
|
||||
* ! Do not edit manually
|
||||
* * The file has been auto-generated using Orval for SigNoz
|
||||
* * regenerate with 'pnpm generate:api'
|
||||
* SigNoz
|
||||
*/
|
||||
import { useMutation, useQuery } from 'react-query';
|
||||
import type {
|
||||
InvalidateOptions,
|
||||
MutationFunction,
|
||||
QueryClient,
|
||||
QueryFunction,
|
||||
QueryKey,
|
||||
UseMutationOptions,
|
||||
UseMutationResult,
|
||||
UseQueryOptions,
|
||||
UseQueryResult,
|
||||
} from 'react-query';
|
||||
|
||||
import type {
|
||||
PrometheusErrorResponseSchemaDTO,
|
||||
PrometheusQueryParams,
|
||||
PrometheusQueryPostParams,
|
||||
PrometheusQueryRangeParams,
|
||||
PrometheusQueryRangePostParams,
|
||||
PrometheusSuccessResponseSchemaDTO,
|
||||
RenderErrorResponseDTO,
|
||||
} from '../sigNoz.schemas';
|
||||
|
||||
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
|
||||
import type { ErrorType } from '../../../generatedAPIInstance';
|
||||
|
||||
/**
|
||||
* Prometheus-compatible endpoint: the request and response contract is the upstream Prometheus HTTP API (https://prometheus.io/docs/prometheus/latest/querying/api/). Parameters are accepted as URL query parameters or a form-encoded body, on GET and POST alike.
|
||||
* @summary Prometheus instant query
|
||||
*/
|
||||
export const prometheusQuery = (
|
||||
params: PrometheusQueryParams,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<PrometheusSuccessResponseSchemaDTO>({
|
||||
url: `/prometheus/api/v1/query`,
|
||||
method: 'GET',
|
||||
params,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getPrometheusQueryQueryKey = (params?: PrometheusQueryParams) => {
|
||||
return [`/prometheus/api/v1/query`, ...(params ? [params] : [])] as const;
|
||||
};
|
||||
|
||||
export const getPrometheusQueryQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof prometheusQuery>>,
|
||||
TError = ErrorType<PrometheusErrorResponseSchemaDTO | RenderErrorResponseDTO>,
|
||||
>(
|
||||
params: PrometheusQueryParams,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof prometheusQuery>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
) => {
|
||||
const { query: queryOptions } = options ?? {};
|
||||
|
||||
const queryKey = queryOptions?.queryKey ?? getPrometheusQueryQueryKey(params);
|
||||
|
||||
const queryFn: QueryFunction<Awaited<ReturnType<typeof prometheusQuery>>> = ({
|
||||
signal,
|
||||
}) => prometheusQuery(params, signal);
|
||||
|
||||
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof prometheusQuery>>,
|
||||
TError,
|
||||
TData
|
||||
> & { queryKey: QueryKey };
|
||||
};
|
||||
|
||||
export type PrometheusQueryQueryResult = NonNullable<
|
||||
Awaited<ReturnType<typeof prometheusQuery>>
|
||||
>;
|
||||
export type PrometheusQueryQueryError = ErrorType<
|
||||
PrometheusErrorResponseSchemaDTO | RenderErrorResponseDTO
|
||||
>;
|
||||
|
||||
/**
|
||||
* @summary Prometheus instant query
|
||||
*/
|
||||
|
||||
export function usePrometheusQuery<
|
||||
TData = Awaited<ReturnType<typeof prometheusQuery>>,
|
||||
TError = ErrorType<PrometheusErrorResponseSchemaDTO | RenderErrorResponseDTO>,
|
||||
>(
|
||||
params: PrometheusQueryParams,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof prometheusQuery>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getPrometheusQueryQueryOptions(params, options);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
};
|
||||
|
||||
return { ...query, queryKey: queryOptions.queryKey };
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Prometheus instant query
|
||||
*/
|
||||
export const invalidatePrometheusQuery = async (
|
||||
queryClient: QueryClient,
|
||||
params: PrometheusQueryParams,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
{ queryKey: getPrometheusQueryQueryKey(params) },
|
||||
options,
|
||||
);
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
/**
|
||||
* Prometheus-compatible endpoint: the request and response contract is the upstream Prometheus HTTP API (https://prometheus.io/docs/prometheus/latest/querying/api/). Parameters are accepted as URL query parameters or a form-encoded body, on GET and POST alike.
|
||||
* @summary Prometheus instant query
|
||||
*/
|
||||
export const prometheusQueryPost = (
|
||||
params: PrometheusQueryPostParams,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<PrometheusSuccessResponseSchemaDTO>({
|
||||
url: `/prometheus/api/v1/query`,
|
||||
method: 'POST',
|
||||
params,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getPrometheusQueryPostMutationOptions = <
|
||||
TError = ErrorType<PrometheusErrorResponseSchemaDTO | RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof prometheusQueryPost>>,
|
||||
TError,
|
||||
{ params: PrometheusQueryPostParams },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof prometheusQueryPost>>,
|
||||
TError,
|
||||
{ params: PrometheusQueryPostParams },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['prometheusQueryPost'];
|
||||
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 prometheusQueryPost>>,
|
||||
{ params: PrometheusQueryPostParams }
|
||||
> = (props) => {
|
||||
const { params } = props ?? {};
|
||||
|
||||
return prometheusQueryPost(params);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type PrometheusQueryPostMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof prometheusQueryPost>>
|
||||
>;
|
||||
|
||||
export type PrometheusQueryPostMutationError = ErrorType<
|
||||
PrometheusErrorResponseSchemaDTO | RenderErrorResponseDTO
|
||||
>;
|
||||
|
||||
/**
|
||||
* @summary Prometheus instant query
|
||||
*/
|
||||
export const usePrometheusQueryPost = <
|
||||
TError = ErrorType<PrometheusErrorResponseSchemaDTO | RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof prometheusQueryPost>>,
|
||||
TError,
|
||||
{ params: PrometheusQueryPostParams },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof prometheusQueryPost>>,
|
||||
TError,
|
||||
{ params: PrometheusQueryPostParams },
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getPrometheusQueryPostMutationOptions(options));
|
||||
};
|
||||
/**
|
||||
* Prometheus-compatible endpoint: the request and response contract is the upstream Prometheus HTTP API (https://prometheus.io/docs/prometheus/latest/querying/api/). Parameters are accepted as URL query parameters or a form-encoded body, on GET and POST alike.
|
||||
* @summary Prometheus range query
|
||||
*/
|
||||
export const prometheusQueryRange = (
|
||||
params: PrometheusQueryRangeParams,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<PrometheusSuccessResponseSchemaDTO>({
|
||||
url: `/prometheus/api/v1/query_range`,
|
||||
method: 'GET',
|
||||
params,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getPrometheusQueryRangeQueryKey = (
|
||||
params?: PrometheusQueryRangeParams,
|
||||
) => {
|
||||
return [
|
||||
`/prometheus/api/v1/query_range`,
|
||||
...(params ? [params] : []),
|
||||
] as const;
|
||||
};
|
||||
|
||||
export const getPrometheusQueryRangeQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof prometheusQueryRange>>,
|
||||
TError = ErrorType<PrometheusErrorResponseSchemaDTO | RenderErrorResponseDTO>,
|
||||
>(
|
||||
params: PrometheusQueryRangeParams,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof prometheusQueryRange>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
) => {
|
||||
const { query: queryOptions } = options ?? {};
|
||||
|
||||
const queryKey =
|
||||
queryOptions?.queryKey ?? getPrometheusQueryRangeQueryKey(params);
|
||||
|
||||
const queryFn: QueryFunction<
|
||||
Awaited<ReturnType<typeof prometheusQueryRange>>
|
||||
> = ({ signal }) => prometheusQueryRange(params, signal);
|
||||
|
||||
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof prometheusQueryRange>>,
|
||||
TError,
|
||||
TData
|
||||
> & { queryKey: QueryKey };
|
||||
};
|
||||
|
||||
export type PrometheusQueryRangeQueryResult = NonNullable<
|
||||
Awaited<ReturnType<typeof prometheusQueryRange>>
|
||||
>;
|
||||
export type PrometheusQueryRangeQueryError = ErrorType<
|
||||
PrometheusErrorResponseSchemaDTO | RenderErrorResponseDTO
|
||||
>;
|
||||
|
||||
/**
|
||||
* @summary Prometheus range query
|
||||
*/
|
||||
|
||||
export function usePrometheusQueryRange<
|
||||
TData = Awaited<ReturnType<typeof prometheusQueryRange>>,
|
||||
TError = ErrorType<PrometheusErrorResponseSchemaDTO | RenderErrorResponseDTO>,
|
||||
>(
|
||||
params: PrometheusQueryRangeParams,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof prometheusQueryRange>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getPrometheusQueryRangeQueryOptions(params, options);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
};
|
||||
|
||||
return { ...query, queryKey: queryOptions.queryKey };
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Prometheus range query
|
||||
*/
|
||||
export const invalidatePrometheusQueryRange = async (
|
||||
queryClient: QueryClient,
|
||||
params: PrometheusQueryRangeParams,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
{ queryKey: getPrometheusQueryRangeQueryKey(params) },
|
||||
options,
|
||||
);
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
/**
|
||||
* Prometheus-compatible endpoint: the request and response contract is the upstream Prometheus HTTP API (https://prometheus.io/docs/prometheus/latest/querying/api/). Parameters are accepted as URL query parameters or a form-encoded body, on GET and POST alike.
|
||||
* @summary Prometheus range query
|
||||
*/
|
||||
export const prometheusQueryRangePost = (
|
||||
params: PrometheusQueryRangePostParams,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<PrometheusSuccessResponseSchemaDTO>({
|
||||
url: `/prometheus/api/v1/query_range`,
|
||||
method: 'POST',
|
||||
params,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getPrometheusQueryRangePostMutationOptions = <
|
||||
TError = ErrorType<PrometheusErrorResponseSchemaDTO | RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof prometheusQueryRangePost>>,
|
||||
TError,
|
||||
{ params: PrometheusQueryRangePostParams },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof prometheusQueryRangePost>>,
|
||||
TError,
|
||||
{ params: PrometheusQueryRangePostParams },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['prometheusQueryRangePost'];
|
||||
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 prometheusQueryRangePost>>,
|
||||
{ params: PrometheusQueryRangePostParams }
|
||||
> = (props) => {
|
||||
const { params } = props ?? {};
|
||||
|
||||
return prometheusQueryRangePost(params);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type PrometheusQueryRangePostMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof prometheusQueryRangePost>>
|
||||
>;
|
||||
|
||||
export type PrometheusQueryRangePostMutationError = ErrorType<
|
||||
PrometheusErrorResponseSchemaDTO | RenderErrorResponseDTO
|
||||
>;
|
||||
|
||||
/**
|
||||
* @summary Prometheus range query
|
||||
*/
|
||||
export const usePrometheusQueryRangePost = <
|
||||
TError = ErrorType<PrometheusErrorResponseSchemaDTO | RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof prometheusQueryRangePost>>,
|
||||
TError,
|
||||
{ params: PrometheusQueryRangePostParams },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof prometheusQueryRangePost>>,
|
||||
TError,
|
||||
{ params: PrometheusQueryRangePostParams },
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getPrometheusQueryRangePostMutationOptions(options));
|
||||
};
|
||||
@@ -4960,53 +4960,6 @@ export interface DashboardtypesGettablePublicDashboardDataV2DTO {
|
||||
publicDashboard?: DashboardtypesGettablePublicDasbhboardDTO;
|
||||
}
|
||||
|
||||
export interface DashboardtypesGettableSystemDashboardDTO {
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
createdAt?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
createdBy?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
image?: string;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
locked: boolean;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
orgId: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
schemaVersion: string;
|
||||
source: DashboardtypesSourceDTO;
|
||||
spec: DashboardtypesDashboardSpecDTO;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
tags: TagtypesGettableTagDTO[] | null;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
updatedAt?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
updatedBy?: string;
|
||||
}
|
||||
|
||||
export enum DashboardtypesPatchOpDTO {
|
||||
add = 'add',
|
||||
remove = 'remove',
|
||||
@@ -8021,6 +7974,164 @@ export interface PreferencetypesUpdatablePreferenceDTO {
|
||||
value?: unknown;
|
||||
}
|
||||
|
||||
export enum PrometheusErrorResponseSchemaDTOErrorType {
|
||||
bad_data = 'bad_data',
|
||||
execution = 'execution',
|
||||
canceled = 'canceled',
|
||||
timeout = 'timeout',
|
||||
internal = 'internal',
|
||||
}
|
||||
export enum PrometheusErrorResponseSchemaDTOStatus {
|
||||
error = 'error',
|
||||
}
|
||||
export interface PrometheusErrorResponseSchemaDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
error: string;
|
||||
/**
|
||||
* @enum bad_data,execution,canceled,timeout,internal
|
||||
* @type string
|
||||
*/
|
||||
errorType: PrometheusErrorResponseSchemaDTOErrorType;
|
||||
/**
|
||||
* @enum error
|
||||
* @type string
|
||||
*/
|
||||
status: PrometheusErrorResponseSchemaDTOStatus;
|
||||
}
|
||||
|
||||
export enum PrometheusMatrixDataSchemaDTOResultType {
|
||||
matrix = 'matrix',
|
||||
}
|
||||
export type PrometheusSamplePairSchemaDTOItem = number | string;
|
||||
|
||||
/**
|
||||
* A [timestamp, value] pair: float unix seconds, then the string-encoded sample value ("NaN", "+Inf", "-Inf" included).
|
||||
* @minItems 2
|
||||
* @maxItems 2
|
||||
* @nullable
|
||||
*/
|
||||
export type PrometheusSamplePairSchemaDTO =
|
||||
| PrometheusSamplePairSchemaDTOItem[]
|
||||
| null;
|
||||
|
||||
export type PrometheusMatrixSeriesSchemaDTOMetricAnyOf = {
|
||||
[key: string]: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type PrometheusMatrixSeriesSchemaDTOMetric =
|
||||
PrometheusMatrixSeriesSchemaDTOMetricAnyOf | null;
|
||||
|
||||
export interface PrometheusMatrixSeriesSchemaDTO {
|
||||
/**
|
||||
* @type object,null
|
||||
*/
|
||||
metric: PrometheusMatrixSeriesSchemaDTOMetric;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
values: (PrometheusSamplePairSchemaDTO | null)[] | null;
|
||||
}
|
||||
|
||||
export interface PrometheusMatrixDataSchemaDTO {
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
result: PrometheusMatrixSeriesSchemaDTO[] | null;
|
||||
/**
|
||||
* @enum matrix
|
||||
* @type string
|
||||
*/
|
||||
resultType: PrometheusMatrixDataSchemaDTOResultType;
|
||||
}
|
||||
|
||||
export type PrometheusVectorSampleSchemaDTOMetricAnyOf = {
|
||||
[key: string]: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type PrometheusVectorSampleSchemaDTOMetric =
|
||||
PrometheusVectorSampleSchemaDTOMetricAnyOf | null;
|
||||
|
||||
export interface PrometheusVectorSampleSchemaDTO {
|
||||
/**
|
||||
* @type object,null
|
||||
*/
|
||||
metric: PrometheusVectorSampleSchemaDTOMetric;
|
||||
value: PrometheusSamplePairSchemaDTO | null;
|
||||
}
|
||||
|
||||
export enum PrometheusVectorDataSchemaDTOResultType {
|
||||
vector = 'vector',
|
||||
}
|
||||
export interface PrometheusVectorDataSchemaDTO {
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
result: PrometheusVectorSampleSchemaDTO[] | null;
|
||||
/**
|
||||
* @enum vector
|
||||
* @type string
|
||||
*/
|
||||
resultType: PrometheusVectorDataSchemaDTOResultType;
|
||||
}
|
||||
|
||||
export enum PrometheusScalarDataSchemaDTOResultType {
|
||||
scalar = 'scalar',
|
||||
}
|
||||
export interface PrometheusScalarDataSchemaDTO {
|
||||
result: PrometheusSamplePairSchemaDTO | null;
|
||||
/**
|
||||
* @enum scalar
|
||||
* @type string
|
||||
*/
|
||||
resultType: PrometheusScalarDataSchemaDTOResultType;
|
||||
}
|
||||
|
||||
export enum PrometheusStringDataSchemaDTOResultType {
|
||||
string = 'string',
|
||||
}
|
||||
export interface PrometheusStringDataSchemaDTO {
|
||||
result: PrometheusSamplePairSchemaDTO | null;
|
||||
/**
|
||||
* @enum string
|
||||
* @type string
|
||||
*/
|
||||
resultType: PrometheusStringDataSchemaDTOResultType;
|
||||
}
|
||||
|
||||
export type PrometheusQueryDataSchemaDTO =
|
||||
| PrometheusMatrixDataSchemaDTO
|
||||
| PrometheusVectorDataSchemaDTO
|
||||
| PrometheusScalarDataSchemaDTO
|
||||
| PrometheusStringDataSchemaDTO;
|
||||
|
||||
export enum PrometheusSuccessResponseSchemaDTOStatus {
|
||||
success = 'success',
|
||||
}
|
||||
export interface PrometheusSuccessResponseSchemaDTO {
|
||||
data: PrometheusQueryDataSchemaDTO;
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
infos?: string[];
|
||||
/**
|
||||
* @enum success
|
||||
* @type string
|
||||
*/
|
||||
status: PrometheusSuccessResponseSchemaDTOStatus;
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
warnings?: string[];
|
||||
}
|
||||
|
||||
export interface PromotetypesWrappedIndexDTO {
|
||||
fieldDataType?: TelemetrytypesFieldDataTypeDTO;
|
||||
/**
|
||||
@@ -11360,17 +11471,6 @@ export type MigrateDashboardV2200 = {
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type GetSystemDashboardPathParameters = {
|
||||
name: string;
|
||||
};
|
||||
export type GetSystemDashboard200 = {
|
||||
data: DashboardtypesGettableSystemDashboardDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type GetFeatures200 = {
|
||||
/**
|
||||
* @type array
|
||||
@@ -12529,3 +12629,115 @@ export type ReplaceVariables200 = {
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type PrometheusQueryParams = {
|
||||
/**
|
||||
* @type string
|
||||
* @description PromQL expression.
|
||||
*/
|
||||
query: string;
|
||||
/**
|
||||
* @type string
|
||||
* @description Evaluation timestamp: RFC3339 or float unix seconds. Defaults to the server's current time.
|
||||
*/
|
||||
time?: string;
|
||||
/**
|
||||
* @type string
|
||||
* @description Evaluation timeout: duration string or float seconds.
|
||||
*/
|
||||
timeout?: string;
|
||||
/**
|
||||
* @type string
|
||||
* @description Any non-empty value includes query statistics in the response.
|
||||
*/
|
||||
stats?: string;
|
||||
};
|
||||
|
||||
export type PrometheusQueryPostParams = {
|
||||
/**
|
||||
* @type string
|
||||
* @description PromQL expression.
|
||||
*/
|
||||
query: string;
|
||||
/**
|
||||
* @type string
|
||||
* @description Evaluation timestamp: RFC3339 or float unix seconds. Defaults to the server's current time.
|
||||
*/
|
||||
time?: string;
|
||||
/**
|
||||
* @type string
|
||||
* @description Evaluation timeout: duration string or float seconds.
|
||||
*/
|
||||
timeout?: string;
|
||||
/**
|
||||
* @type string
|
||||
* @description Any non-empty value includes query statistics in the response.
|
||||
*/
|
||||
stats?: string;
|
||||
};
|
||||
|
||||
export type PrometheusQueryRangeParams = {
|
||||
/**
|
||||
* @type string
|
||||
* @description PromQL expression.
|
||||
*/
|
||||
query: string;
|
||||
/**
|
||||
* @type string
|
||||
* @description Range start: RFC3339 or float unix seconds.
|
||||
*/
|
||||
start: string;
|
||||
/**
|
||||
* @type string
|
||||
* @description Range end: RFC3339 or float unix seconds.
|
||||
*/
|
||||
end: string;
|
||||
/**
|
||||
* @type string
|
||||
* @description Resolution step: duration string or float seconds.
|
||||
*/
|
||||
step: string;
|
||||
/**
|
||||
* @type string
|
||||
* @description Evaluation timeout: duration string or float seconds.
|
||||
*/
|
||||
timeout?: string;
|
||||
/**
|
||||
* @type string
|
||||
* @description Any non-empty value includes query statistics in the response.
|
||||
*/
|
||||
stats?: string;
|
||||
};
|
||||
|
||||
export type PrometheusQueryRangePostParams = {
|
||||
/**
|
||||
* @type string
|
||||
* @description PromQL expression.
|
||||
*/
|
||||
query: string;
|
||||
/**
|
||||
* @type string
|
||||
* @description Range start: RFC3339 or float unix seconds.
|
||||
*/
|
||||
start: string;
|
||||
/**
|
||||
* @type string
|
||||
* @description Range end: RFC3339 or float unix seconds.
|
||||
*/
|
||||
end: string;
|
||||
/**
|
||||
* @type string
|
||||
* @description Resolution step: duration string or float seconds.
|
||||
*/
|
||||
step: string;
|
||||
/**
|
||||
* @type string
|
||||
* @description Evaluation timeout: duration string or float seconds.
|
||||
*/
|
||||
timeout?: string;
|
||||
/**
|
||||
* @type string
|
||||
* @description Any non-empty value includes query statistics in the response.
|
||||
*/
|
||||
stats?: string;
|
||||
};
|
||||
|
||||
@@ -332,33 +332,6 @@ func (provider *provider) addDashboardRoutes(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/dashboards/system/{name}", handler.New(
|
||||
provider.authzMiddleware.CheckResources(provider.dashboardHandler.GetSystemDashboard, authtypes.SigNozAdminRoleName, authtypes.SigNozEditorRoleName, authtypes.SigNozViewerRoleName),
|
||||
handler.OpenAPIDef{
|
||||
ID: "GetSystemDashboard",
|
||||
Tags: []string{"dashboard"},
|
||||
Summary: "Get system dashboard",
|
||||
Description: "Returns a dashboard SigNoz ships and owns, addressed by its stable definition name (e.g. `ai-o11y-overview`) rather than its id. System dashboards are read-only and upgraded through releases. The dashboard's own `name` field carries a reserved prefix that the path segment must not include.",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: new(dashboardtypes.GettableSystemDashboard),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceDashboard.Scope(coretypes.VerbRead)}),
|
||||
},
|
||||
handler.WithResourceDefs(handler.BasicResourceDef{
|
||||
Resource: coretypes.ResourceMetaResourceDashboard,
|
||||
Verb: coretypes.VerbRead,
|
||||
Category: coretypes.ActionCategoryDataAccess,
|
||||
ID: provider.systemDashboardID(),
|
||||
Selector: coretypes.IDSelector,
|
||||
}),
|
||||
)).Methods(http.MethodGet).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Pinning mutates the calling user's pin list, not the dashboard, so it rides
|
||||
// on the collection-level list permission rather than a per-dashboard check.
|
||||
// The id is still extracted, for audit.
|
||||
@@ -745,23 +718,3 @@ func (provider *provider) addDashboardRoutes(router *mux.Router) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// systemDashboardID resolves the {name} path param to the dashboard's id. Authz
|
||||
// tuples and audit records are written against ids, so the name has to be
|
||||
// resolved before either runs.
|
||||
func (provider *provider) systemDashboardID() coretypes.ResourceIDExtractor {
|
||||
return coretypes.NewResourceIDExtractor(coretypes.PhaseRequest, func(ec coretypes.ExtractorContext) (string, error) {
|
||||
ctx := ec.Request.Context()
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
systemDashboard, err := provider.dashboardModule.GetSystemDashboard(ctx, valuer.MustNewUUID(claims.OrgID), mux.Vars(ec.Request)["name"])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return systemDashboard.ID.StringValue(), nil
|
||||
})
|
||||
}
|
||||
|
||||
102
pkg/apiserver/signozapiserver/prometheus.go
Normal file
102
pkg/apiserver/signozapiserver/prometheus.go
Normal file
@@ -0,0 +1,102 @@
|
||||
package signozapiserver
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/http/handler"
|
||||
"github.com/SigNoz/signoz/pkg/http/render"
|
||||
"github.com/SigNoz/signoz/pkg/prometheus"
|
||||
"github.com/SigNoz/signoz/pkg/querybuilder"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
"github.com/gorilla/mux"
|
||||
openapi "github.com/swaggest/openapi-go"
|
||||
)
|
||||
|
||||
// prometheusOpenAPIHandler skips the default handler wrapper: that wraps
|
||||
// every response in the house envelope, and these endpoints follow
|
||||
// Prometheus' wire contract, described by the prometheus package's *Schema
|
||||
// types.
|
||||
type prometheusOpenAPIHandler struct {
|
||||
handlerFunc http.HandlerFunc
|
||||
id string
|
||||
summary string
|
||||
params any
|
||||
}
|
||||
|
||||
func (h *prometheusOpenAPIHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
h.handlerFunc.ServeHTTP(rw, req)
|
||||
}
|
||||
|
||||
func (h *prometheusOpenAPIHandler) ServeOpenAPI(opCtx openapi.OperationContext) {
|
||||
// One route serves GET and POST; operation IDs must stay unique.
|
||||
id := h.id
|
||||
if strings.EqualFold(opCtx.Method(), http.MethodPost) {
|
||||
id += "Post"
|
||||
}
|
||||
opCtx.SetID(id)
|
||||
opCtx.SetTags("prometheus")
|
||||
opCtx.SetSummary(h.summary)
|
||||
opCtx.SetDescription("Prometheus-compatible endpoint: the request and response contract is the upstream Prometheus HTTP API (https://prometheus.io/docs/prometheus/latest/querying/api/). Parameters are accepted as URL query parameters or a form-encoded body, on GET and POST alike.")
|
||||
|
||||
for _, scheme := range newScopedSecuritySchemes([]string{coretypes.ResourceTelemetryResourceMetrics.Scope(coretypes.VerbRead)}) {
|
||||
opCtx.AddSecurity(scheme.Name, scheme.Scopes...)
|
||||
}
|
||||
|
||||
opCtx.AddReqStructure(h.params)
|
||||
|
||||
opCtx.AddRespStructure(
|
||||
prometheus.SuccessResponseSchema{},
|
||||
openapi.WithContentType("application/json"),
|
||||
openapi.WithHTTPStatus(http.StatusOK),
|
||||
)
|
||||
for _, statusCode := range []int{http.StatusBadRequest, http.StatusUnprocessableEntity, http.StatusServiceUnavailable, http.StatusInternalServerError} {
|
||||
opCtx.AddRespStructure(
|
||||
prometheus.ErrorResponseSchema{},
|
||||
openapi.WithContentType("application/json"),
|
||||
openapi.WithHTTPStatus(statusCode),
|
||||
)
|
||||
}
|
||||
// The auth middleware answers before the handler and uses the house
|
||||
// envelope, not Prometheus'.
|
||||
for _, statusCode := range []int{http.StatusUnauthorized, http.StatusForbidden} {
|
||||
opCtx.AddRespStructure(
|
||||
render.ErrorResponse{Status: render.StatusError.String(), Error: &errors.JSON{}},
|
||||
openapi.WithContentType("application/json"),
|
||||
openapi.WithHTTPStatus(statusCode),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *prometheusOpenAPIHandler) ResourceDefs() []handler.ResourceDef {
|
||||
return []handler.ResourceDef{handler.TelemetryResourceDef{
|
||||
Verb: coretypes.VerbRead,
|
||||
Category: coretypes.ActionCategoryDataAccess,
|
||||
Selector: querybuilder.TelemetrySelector,
|
||||
Resources: querybuilder.PromQLResources,
|
||||
}}
|
||||
}
|
||||
|
||||
func (provider *provider) addPrometheusRoutes(router *mux.Router) error {
|
||||
if err := router.Handle("/prometheus/api/v1/query", &prometheusOpenAPIHandler{
|
||||
handlerFunc: provider.authzMiddleware.CheckResources(provider.prometheusHandler.Query, authtypes.SigNozAdminRoleName, authtypes.SigNozEditorRoleName, authtypes.SigNozViewerRoleName),
|
||||
id: "PrometheusQuery",
|
||||
summary: "Prometheus instant query",
|
||||
params: new(prometheus.QueryParamsSchema),
|
||||
}).Methods(http.MethodGet, http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/prometheus/api/v1/query_range", &prometheusOpenAPIHandler{
|
||||
handlerFunc: provider.authzMiddleware.CheckResources(provider.prometheusHandler.QueryRange, authtypes.SigNozAdminRoleName, authtypes.SigNozEditorRoleName, authtypes.SigNozViewerRoleName),
|
||||
id: "PrometheusQueryRange",
|
||||
summary: "Prometheus range query",
|
||||
params: new(prometheus.QueryRangeParamsSchema),
|
||||
}).Methods(http.MethodGet, http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -32,6 +32,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/modules/spanmapper"
|
||||
"github.com/SigNoz/signoz/pkg/modules/tracedetail"
|
||||
"github.com/SigNoz/signoz/pkg/modules/user"
|
||||
"github.com/SigNoz/signoz/pkg/prometheus"
|
||||
"github.com/SigNoz/signoz/pkg/querier"
|
||||
"github.com/SigNoz/signoz/pkg/ruler"
|
||||
"github.com/SigNoz/signoz/pkg/statsreporter"
|
||||
@@ -75,6 +76,7 @@ type provider struct {
|
||||
ruleStateHistoryHandler rulestatehistory.Handler
|
||||
spanMapperHandler spanmapper.Handler
|
||||
alertmanagerHandler alertmanager.Handler
|
||||
prometheusHandler prometheus.Handler
|
||||
traceDetailHandler tracedetail.Handler
|
||||
rulerHandler ruler.Handler
|
||||
llmPricingRuleHandler llmpricingrule.Handler
|
||||
@@ -113,6 +115,7 @@ func NewFactory(
|
||||
ruleStateHistoryHandler rulestatehistory.Handler,
|
||||
spanMapperHandler spanmapper.Handler,
|
||||
alertmanagerHandler alertmanager.Handler,
|
||||
prometheusHandler prometheus.Handler,
|
||||
llmPricingRuleHandler llmpricingrule.Handler,
|
||||
traceDetailHandler tracedetail.Handler,
|
||||
rulerHandler ruler.Handler,
|
||||
@@ -154,6 +157,7 @@ func NewFactory(
|
||||
ruleStateHistoryHandler,
|
||||
spanMapperHandler,
|
||||
alertmanagerHandler,
|
||||
prometheusHandler,
|
||||
llmPricingRuleHandler,
|
||||
traceDetailHandler,
|
||||
rulerHandler,
|
||||
@@ -197,6 +201,7 @@ func newProvider(
|
||||
ruleStateHistoryHandler rulestatehistory.Handler,
|
||||
spanMapperHandler spanmapper.Handler,
|
||||
alertmanagerHandler alertmanager.Handler,
|
||||
prometheusHandler prometheus.Handler,
|
||||
llmPricingRuleHandler llmpricingrule.Handler,
|
||||
traceDetailHandler tracedetail.Handler,
|
||||
rulerHandler ruler.Handler,
|
||||
@@ -239,6 +244,7 @@ func newProvider(
|
||||
ruleStateHistoryHandler: ruleStateHistoryHandler,
|
||||
spanMapperHandler: spanMapperHandler,
|
||||
alertmanagerHandler: alertmanagerHandler,
|
||||
prometheusHandler: prometheusHandler,
|
||||
traceDetailHandler: traceDetailHandler,
|
||||
rulerHandler: rulerHandler,
|
||||
llmPricingRuleHandler: llmPricingRuleHandler,
|
||||
@@ -340,6 +346,10 @@ func (provider *provider) AddToRouter(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := provider.addPrometheusRoutes(router); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := provider.addServiceAccountRoutes(router); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -99,14 +99,6 @@ type Module interface {
|
||||
DeleteView(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error
|
||||
|
||||
GetByMetricNamesV2(ctx context.Context, orgID valuer.UUID, metricNames []string) (map[string][]dashboardtypes.DashboardPanelRef, error)
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
// System dashboard methods
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
|
||||
ReconcileSystemDashboards(ctx context.Context, orgID valuer.UUID) error
|
||||
|
||||
GetSystemDashboard(ctx context.Context, orgID valuer.UUID, name string) (*dashboardtypes.DashboardV2, error)
|
||||
}
|
||||
|
||||
type Handler interface {
|
||||
@@ -170,6 +162,4 @@ type Handler interface {
|
||||
UpdateView(http.ResponseWriter, *http.Request)
|
||||
|
||||
DeleteView(http.ResponseWriter, *http.Request)
|
||||
|
||||
GetSystemDashboard(http.ResponseWriter, *http.Request)
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"version": 1,
|
||||
"definition": {
|
||||
"schemaVersion": "v6",
|
||||
"name": "signoz---ai-o11y-overview",
|
||||
"tags": [],
|
||||
"spec": {
|
||||
"display": {
|
||||
"name": "AI Observability Overview",
|
||||
"description": "Overview of LLM traffic. Panels ship in an upcoming release."
|
||||
},
|
||||
"variables": [],
|
||||
"panels": {},
|
||||
"layouts": []
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,25 +21,23 @@ import (
|
||||
)
|
||||
|
||||
type module struct {
|
||||
store dashboardtypes.Store
|
||||
settings factory.ScopedProviderSettings
|
||||
analytics analytics.Analytics
|
||||
orgGetter organization.Getter
|
||||
queryParser queryparser.QueryParser
|
||||
tagModule tag.Module
|
||||
systemDashboardRegistry dashboardtypes.SystemDashboardRegistry
|
||||
store dashboardtypes.Store
|
||||
settings factory.ScopedProviderSettings
|
||||
analytics analytics.Analytics
|
||||
orgGetter organization.Getter
|
||||
queryParser queryparser.QueryParser
|
||||
tagModule tag.Module
|
||||
}
|
||||
|
||||
func NewModule(store dashboardtypes.Store, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, tagModule tag.Module, systemDashboardRegistry dashboardtypes.SystemDashboardRegistry) dashboard.Module {
|
||||
func NewModule(store dashboardtypes.Store, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, tagModule tag.Module) dashboard.Module {
|
||||
scopedProviderSettings := factory.NewScopedProviderSettings(settings, "github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard")
|
||||
return &module{
|
||||
store: store,
|
||||
settings: scopedProviderSettings,
|
||||
analytics: analytics,
|
||||
orgGetter: orgGetter,
|
||||
queryParser: queryParser,
|
||||
tagModule: tagModule,
|
||||
systemDashboardRegistry: systemDashboardRegistry,
|
||||
store: store,
|
||||
settings: scopedProviderSettings,
|
||||
analytics: analytics,
|
||||
orgGetter: orgGetter,
|
||||
queryParser: queryParser,
|
||||
tagModule: tagModule,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ package impldashboard
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
@@ -65,23 +64,6 @@ func (store *store) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID)
|
||||
return storableDashboard, nil
|
||||
}
|
||||
|
||||
func (store *store) GetByName(ctx context.Context, orgID valuer.UUID, name string) (*dashboardtypes.StorableDashboard, error) {
|
||||
storableDashboard := new(dashboardtypes.StorableDashboard)
|
||||
err := store.
|
||||
sqlstore.
|
||||
BunDB().
|
||||
NewSelect().
|
||||
Model(storableDashboard).
|
||||
Where("name = ?", name).
|
||||
Where("org_id = ?", orgID).
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, store.sqlstore.WrapNotFoundErrf(err, errors.CodeNotFound, "dashboard with name %s doesn't exist", name)
|
||||
}
|
||||
|
||||
return storableDashboard, nil
|
||||
}
|
||||
|
||||
// ListForUser emits the joined dashboard ⨝ user_dashboard_preference query the
|
||||
// spec calls for. Aliases:
|
||||
//
|
||||
@@ -631,60 +613,3 @@ func (store *store) DeleteDashboardView(ctx context.Context, orgID valuer.UUID,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (store *store) CreateSystemDashboard(ctx context.Context, storable *dashboardtypes.StorableSystemDashboard) error {
|
||||
_, err := store.
|
||||
sqlstore.
|
||||
BunDBCtx(ctx).
|
||||
NewInsert().
|
||||
Model(storable).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return store.sqlstore.WrapAlreadyExistsErrf(err, dashboardtypes.ErrCodeSystemDashboardAlreadyProvisioned, "system dashboard %s is already provisioned", storable.Name)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (store *store) GetSystemDashboard(ctx context.Context, orgID valuer.UUID, name string) (*dashboardtypes.StorableSystemDashboard, error) {
|
||||
storable := new(dashboardtypes.StorableSystemDashboard)
|
||||
err := store.
|
||||
sqlstore.
|
||||
BunDBCtx(ctx).
|
||||
NewSelect().
|
||||
Model(storable).
|
||||
Where("org_id = ?", orgID).
|
||||
Where("name = ?", name).
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, store.sqlstore.WrapNotFoundErrf(err, dashboardtypes.ErrCodeSystemDashboardNotFound, "system dashboard %s is not provisioned", name)
|
||||
}
|
||||
|
||||
return storable, nil
|
||||
}
|
||||
|
||||
func (store *store) UpdateSystemDashboardVersion(ctx context.Context, orgID valuer.UUID, name string, version int) error {
|
||||
result, err := store.
|
||||
sqlstore.
|
||||
BunDBCtx(ctx).
|
||||
NewUpdate().
|
||||
Model(new(dashboardtypes.StorableSystemDashboard)).
|
||||
Set("version = ?", version).
|
||||
Set("updated_at = ?", time.Now()).
|
||||
Where("org_id = ?", orgID).
|
||||
Where("name = ?", name).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rows, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rows == 0 {
|
||||
return errors.Newf(errors.TypeNotFound, dashboardtypes.ErrCodeSystemDashboardNotFound, "system dashboard %s is not provisioned", name)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
package impldashboard
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"io/fs"
|
||||
"path"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
|
||||
)
|
||||
|
||||
const definitionsRoot = "fs/definitions"
|
||||
|
||||
//go:embed fs/definitions/*.json
|
||||
var definitionFiles embed.FS
|
||||
|
||||
// NewSystemDashboardRegistry parses every embedded definition. Definitions are
|
||||
// build-time assets validated by a test, so a failure here means the binary
|
||||
// shipped broken JSON.
|
||||
func NewSystemDashboardRegistry() (dashboardtypes.SystemDashboardRegistry, error) {
|
||||
entries, err := fs.ReadDir(definitionFiles, definitionsRoot)
|
||||
if err != nil {
|
||||
return dashboardtypes.SystemDashboardRegistry{}, errors.WrapInternalf(err, errors.CodeInternal, "couldn't read system dashboard definitions")
|
||||
}
|
||||
|
||||
definitions := make([]dashboardtypes.SystemDashboardDefinition, 0, len(entries))
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
file := path.Join(definitionsRoot, entry.Name())
|
||||
raw, err := definitionFiles.ReadFile(file)
|
||||
if err != nil {
|
||||
return dashboardtypes.SystemDashboardRegistry{}, errors.WrapInternalf(err, errors.CodeInternal, "couldn't read %s", file)
|
||||
}
|
||||
|
||||
definition, err := dashboardtypes.NewSystemDashboardDefinition(raw)
|
||||
if err != nil {
|
||||
return dashboardtypes.SystemDashboardRegistry{}, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "couldn't parse %s", file)
|
||||
}
|
||||
definitions = append(definitions, definition)
|
||||
}
|
||||
|
||||
return dashboardtypes.NewSystemDashboardRegistry(definitions)
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
package impldashboard
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/modules/dashboard"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||
)
|
||||
|
||||
const reconcileRetryInterval = 30 * time.Second
|
||||
|
||||
type service struct {
|
||||
settings factory.ScopedProviderSettings
|
||||
module dashboard.Module
|
||||
orgGetter organization.Getter
|
||||
stopC chan struct{}
|
||||
healthyC chan struct{}
|
||||
}
|
||||
|
||||
// NewService reconciles every org's system dashboards once at startup. Orgs
|
||||
// created later are reconciled by the organization setter instead.
|
||||
func NewService(providerSettings factory.ProviderSettings, module dashboard.Module, orgGetter organization.Getter) factory.Service {
|
||||
return &service{
|
||||
settings: factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard"),
|
||||
module: module,
|
||||
orgGetter: orgGetter,
|
||||
stopC: make(chan struct{}),
|
||||
healthyC: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (service *service) Start(ctx context.Context) error {
|
||||
ticker := time.NewTicker(reconcileRetryInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
err := service.reconcile(ctx)
|
||||
if err == nil {
|
||||
close(service.healthyC)
|
||||
<-service.stopC
|
||||
return nil
|
||||
}
|
||||
|
||||
service.settings.Logger().WarnContext(ctx, "system dashboard reconciliation failed, retrying", errors.Attr(err))
|
||||
|
||||
select {
|
||||
case <-service.stopC:
|
||||
return nil
|
||||
case <-ticker.C:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (service *service) Healthy() <-chan struct{} {
|
||||
return service.healthyC
|
||||
}
|
||||
|
||||
func (service *service) Stop(_ context.Context) error {
|
||||
close(service.stopC)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (service *service) reconcile(ctx context.Context) error {
|
||||
orgs, err := service.orgGetter.ListByOwnedKeyRange(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, org := range orgs {
|
||||
if err := service.module.ReconcileSystemDashboards(ctx, org.ID); err != nil {
|
||||
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "couldn't reconcile system dashboards for org %s", org.ID.StringValue())
|
||||
}
|
||||
}
|
||||
|
||||
service.settings.Logger().InfoContext(ctx, "system dashboard reconciliation completed", slog.Int("orgs", len(orgs)))
|
||||
return nil
|
||||
}
|
||||
@@ -502,28 +502,3 @@ func (handler *handler) GetPublicWidgetQueryRangeV2(rw http.ResponseWriter, r *h
|
||||
|
||||
render.Success(rw, http.StatusOK, queryRangeResults)
|
||||
}
|
||||
|
||||
func (handler *handler) GetSystemDashboard(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
name := mux.Vars(r)["name"]
|
||||
if name == "" {
|
||||
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "name is missing in the path"))
|
||||
return
|
||||
}
|
||||
|
||||
systemDashboard, err := handler.module.GetSystemDashboard(ctx, valuer.MustNewUUID(claims.OrgID), name)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusOK, systemDashboard.ToGettableSystemDashboard())
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@ package impldashboard
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/transition"
|
||||
@@ -21,12 +19,9 @@ func (m *module) CreateV2(ctx context.Context, orgID valuer.UUID, createdBy stri
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dashboard, err := postable.NewDashboardV2(orgID, createdBy, source)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dashboard := postable.NewDashboardV2(orgID, createdBy, source)
|
||||
|
||||
err = m.store.RunInTx(ctx, func(ctx context.Context) error {
|
||||
err := m.store.RunInTx(ctx, func(ctx context.Context) error {
|
||||
resolvedTags, err := m.tagModule.SyncTags(ctx, orgID, coretypes.KindDashboard, dashboard.ID, postable.Tags)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -125,20 +120,6 @@ func (module *module) GetV2(ctx context.Context, orgID valuer.UUID, id valuer.UU
|
||||
return storable.ToDashboardV2(tags)
|
||||
}
|
||||
|
||||
func (module *module) getByNameV2(ctx context.Context, orgID valuer.UUID, name string) (*dashboardtypes.DashboardV2, error) {
|
||||
storable, err := module.store.GetByName(ctx, orgID, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tags, err := module.tagModule.ListForResource(ctx, orgID, coretypes.KindDashboard, storable.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return storable.ToDashboardV2(tags)
|
||||
}
|
||||
|
||||
// MigrateV2 retries the v1→v2 migration on a dashboard still stored as v1 (one the
|
||||
// bulk 103 migration skipped or failed). Idempotent: an already-v2 one is unchanged.
|
||||
func (module *module) MigrateV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*dashboardtypes.DashboardV2, error) {
|
||||
@@ -198,33 +179,13 @@ func (module *module) UpdateV2(ctx context.Context, orgID valuer.UUID, id valuer
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return module.updateV2(ctx, orgID, existing, updatedBy, updatable, existing.Update)
|
||||
}
|
||||
|
||||
// updateUnsafeV2 updates a dashboard bypassing the guards. Intended for internal system callers.
|
||||
func (module *module) updateUnsafeV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, updatable dashboardtypes.UpdatableDashboardV2) (*dashboardtypes.DashboardV2, error) {
|
||||
if err := updatable.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
existing, err := module.GetV2(ctx, orgID, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return module.updateV2(ctx, orgID, existing, updatedBy, updatable, existing.UpdateUnsafe)
|
||||
}
|
||||
|
||||
// apply is existing.Update or existing.UpdateUnsafe, so the gated path keeps its
|
||||
// in-transaction checks and only updateUnsafeV2 skips them.
|
||||
func (module *module) updateV2(ctx context.Context, orgID valuer.UUID, existing *dashboardtypes.DashboardV2, updatedBy string, updatable dashboardtypes.UpdatableDashboardV2, apply func(dashboardtypes.UpdatableDashboardV2, string, []*tagtypes.Tag) error) (*dashboardtypes.DashboardV2, error) {
|
||||
err := module.store.RunInTx(ctx, func(ctx context.Context) error {
|
||||
resolvedTags, err := module.tagModule.SyncTags(ctx, orgID, coretypes.KindDashboard, existing.ID, updatable.Tags)
|
||||
err = module.store.RunInTx(ctx, func(ctx context.Context) error {
|
||||
resolvedTags, err := module.tagModule.SyncTags(ctx, orgID, coretypes.KindDashboard, id, updatable.Tags)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = apply(updatable, updatedBy, resolvedTags)
|
||||
err = existing.Update(updatable, updatedBy, resolvedTags)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -335,98 +296,3 @@ func (module *module) UnpinV2(ctx context.Context, orgID valuer.UUID, userID val
|
||||
func (module *module) DeletePreferencesForUser(ctx context.Context, orgID valuer.UUID, userID valuer.UUID) error {
|
||||
return module.store.DeletePreferencesForUser(ctx, orgID, userID)
|
||||
}
|
||||
|
||||
func (m *module) ReconcileSystemDashboards(ctx context.Context, orgID valuer.UUID) error {
|
||||
for _, definition := range m.systemDashboardRegistry.List() {
|
||||
if err := m.reconcileSystemDashboard(ctx, orgID, definition); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *module) reconcileSystemDashboard(ctx context.Context, orgID valuer.UUID, definition dashboardtypes.SystemDashboardDefinition) error {
|
||||
existing, err := m.getByNameV2(ctx, orgID, definition.Name())
|
||||
if err != nil {
|
||||
if !errors.Ast(err, errors.TypeNotFound) {
|
||||
return err
|
||||
}
|
||||
return m.provisionSystemDashboard(ctx, orgID, definition)
|
||||
}
|
||||
|
||||
state, err := m.store.GetSystemDashboard(ctx, orgID, definition.Name())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Only ever move forward: a downgrade must not rewrite the newer content.
|
||||
if state.Version >= definition.Version {
|
||||
return nil
|
||||
}
|
||||
|
||||
return m.upgradeSystemDashboard(ctx, orgID, existing.ID, definition)
|
||||
}
|
||||
|
||||
// provisionSystemDashboard creates the dashboard and its state row in one transaction,
|
||||
// so a system dashboard can never exist without the version it was provisioned at.
|
||||
// A concurrent provisioner (another replica, or the org-creation hook racing the
|
||||
// startup sweep) loses on the state row's unique (org_id, name) index and rolls back.
|
||||
func (m *module) provisionSystemDashboard(ctx context.Context, orgID valuer.UUID, definition dashboardtypes.SystemDashboardDefinition) error {
|
||||
err := m.store.RunInTx(ctx, func(ctx context.Context) error {
|
||||
created, err := m.CreateV2(
|
||||
ctx,
|
||||
orgID,
|
||||
dashboardtypes.ProvisionerIdentity,
|
||||
valuer.UUID{},
|
||||
dashboardtypes.SourceSystem,
|
||||
definition.Dashboard,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return m.store.CreateSystemDashboard(ctx, dashboardtypes.NewStorableSystemDashboard(orgID, created.ID, definition.Name(), definition.Version))
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Ast(err, errors.TypeAlreadyExists) {
|
||||
m.settings.Logger().DebugContext(ctx, "system dashboard already provisioned concurrently", slog.String("name", definition.Name()), slog.String("org_id", orgID.StringValue()))
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
m.settings.Logger().InfoContext(ctx, "provisioned system dashboard", slog.String("name", definition.Name()), slog.Int("version", definition.Version), slog.String("org_id", orgID.StringValue()))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *module) upgradeSystemDashboard(ctx context.Context, orgID valuer.UUID, id valuer.UUID, definition dashboardtypes.SystemDashboardDefinition) error {
|
||||
err := m.store.RunInTx(ctx, func(ctx context.Context) error {
|
||||
if _, err := m.updateUnsafeV2(ctx, orgID, id, dashboardtypes.ProvisionerIdentity, definition.ToUpdatable()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return m.store.UpdateSystemDashboardVersion(ctx, orgID, definition.Name(), definition.Version)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.settings.Logger().InfoContext(ctx, "upgraded system dashboard", slog.String("name", definition.Name()), slog.Int("version", definition.Version), slog.String("org_id", orgID.StringValue()))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *module) GetSystemDashboard(ctx context.Context, orgID valuer.UUID, name string) (*dashboardtypes.DashboardV2, error) {
|
||||
if strings.HasPrefix(name, dashboardtypes.SystemDashboardNamePrefix) {
|
||||
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "name must not carry the %q prefix", dashboardtypes.SystemDashboardNamePrefix)
|
||||
}
|
||||
|
||||
existing, err := m.getByNameV2(ctx, orgID, dashboardtypes.SystemDashboardNamePrefix+name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := existing.ErrIfNotSystem(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return existing, nil
|
||||
}
|
||||
|
||||
@@ -1,191 +0,0 @@
|
||||
package impldashboard
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/analytics/analyticstest"
|
||||
"github.com/SigNoz/signoz/pkg/factory/factorytest"
|
||||
"github.com/SigNoz/signoz/pkg/modules/tag/impltag"
|
||||
"github.com/SigNoz/signoz/pkg/queryparser"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore/sqlitesqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/tagtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const testDashboardName = "test-overview"
|
||||
|
||||
func newTestSQLStore(t *testing.T) sqlstore.SQLStore {
|
||||
t.Helper()
|
||||
|
||||
store, err := sqlitesqlstore.New(context.Background(), factorytest.NewSettings(), sqlstore.Config{
|
||||
Provider: "sqlite",
|
||||
Connection: sqlstore.ConnectionConfig{MaxOpenConns: 10},
|
||||
Sqlite: sqlstore.SqliteConfig{
|
||||
Path: filepath.Join(t.TempDir(), "test.db"),
|
||||
Mode: "wal",
|
||||
BusyTimeout: 5 * time.Second,
|
||||
TransactionMode: "deferred",
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, model := range []any{
|
||||
(*dashboardtypes.StorableDashboard)(nil),
|
||||
(*tagtypes.Tag)(nil),
|
||||
(*tagtypes.TagRelation)(nil),
|
||||
(*dashboardtypes.StorableSystemDashboard)(nil),
|
||||
} {
|
||||
_, err := store.BunDB().NewCreateTable().Model(model).IfNotExists().Exec(context.Background())
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
_, err = store.BunDB().Exec(`CREATE UNIQUE INDEX IF NOT EXISTS uq_system_dashboard_org_name ON system_dashboard (org_id, name)`)
|
||||
require.NoError(t, err)
|
||||
|
||||
return store
|
||||
}
|
||||
|
||||
func newTestModule(t *testing.T, sqlStore sqlstore.SQLStore, definitions ...dashboardtypes.SystemDashboardDefinition) *module {
|
||||
t.Helper()
|
||||
|
||||
registry, err := dashboardtypes.NewSystemDashboardRegistry(definitions)
|
||||
require.NoError(t, err)
|
||||
|
||||
providerSettings := factorytest.NewSettings()
|
||||
return NewModule(
|
||||
NewStore(sqlStore),
|
||||
providerSettings,
|
||||
analyticstest.New(),
|
||||
nil,
|
||||
queryparser.New(providerSettings),
|
||||
impltag.NewModule(impltag.NewStore(sqlStore)),
|
||||
registry,
|
||||
).(*module)
|
||||
}
|
||||
|
||||
func newTestDefinition(t *testing.T, version int, displayName string) dashboardtypes.SystemDashboardDefinition {
|
||||
t.Helper()
|
||||
|
||||
raw := `{
|
||||
"version": ` + strconv.Itoa(version) + `,
|
||||
"definition": {
|
||||
"schemaVersion": "` + dashboardtypes.SchemaVersion + `",
|
||||
"name": "` + dashboardtypes.SystemDashboardNamePrefix + testDashboardName + `",
|
||||
"tags": [],
|
||||
"spec": {"display": {"name": "` + displayName + `"}, "variables": [], "panels": {}, "layouts": []}
|
||||
}
|
||||
}`
|
||||
|
||||
definition, err := dashboardtypes.NewSystemDashboardDefinition([]byte(raw))
|
||||
require.NoError(t, err)
|
||||
|
||||
return definition
|
||||
}
|
||||
|
||||
func TestReconcileProvisionsThenUpgrades(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
sqlStore := newTestSQLStore(t)
|
||||
orgID := valuer.GenerateUUID()
|
||||
|
||||
dashboardModule := newTestModule(t, sqlStore, newTestDefinition(t, 1, "v1"))
|
||||
require.NoError(t, dashboardModule.ReconcileSystemDashboards(ctx, orgID))
|
||||
|
||||
provisioned, err := dashboardModule.GetSystemDashboard(ctx, orgID, testDashboardName)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, dashboardtypes.SourceSystem, provisioned.Source)
|
||||
assert.Equal(t, dashboardtypes.ProvisionerIdentity, provisioned.CreatedBy)
|
||||
assert.Equal(t, "v1", provisioned.Spec.Display.Name)
|
||||
assert.Equal(t, 1, stateVersion(t, dashboardModule, ctx, orgID))
|
||||
|
||||
// Reconciling the same version again is a no-op.
|
||||
require.NoError(t, dashboardModule.ReconcileSystemDashboards(ctx, orgID))
|
||||
unchanged, err := dashboardModule.GetSystemDashboard(ctx, orgID, testDashboardName)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, provisioned.UpdatedAt, unchanged.UpdatedAt)
|
||||
|
||||
// An unmodified copy is upgraded in place, keeping its id.
|
||||
upgradingModule := newTestModule(t, sqlStore, newTestDefinition(t, 2, "v2"))
|
||||
require.NoError(t, upgradingModule.ReconcileSystemDashboards(ctx, orgID))
|
||||
|
||||
upgraded, err := upgradingModule.GetSystemDashboard(ctx, orgID, testDashboardName)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, provisioned.ID, upgraded.ID)
|
||||
assert.Equal(t, "v2", upgraded.Spec.Display.Name)
|
||||
assert.Equal(t, 2, stateVersion(t, upgradingModule, ctx, orgID))
|
||||
}
|
||||
|
||||
func stateVersion(t *testing.T, module *module, ctx context.Context, orgID valuer.UUID) int {
|
||||
t.Helper()
|
||||
|
||||
state, err := module.store.GetSystemDashboard(ctx, orgID, dashboardtypes.SystemDashboardNamePrefix+testDashboardName)
|
||||
require.NoError(t, err)
|
||||
|
||||
return state.Version
|
||||
}
|
||||
|
||||
func TestSystemDashboardsAreImmutableToUsers(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
sqlStore := newTestSQLStore(t)
|
||||
orgID := valuer.GenerateUUID()
|
||||
|
||||
dashboardModule := newTestModule(t, sqlStore, newTestDefinition(t, 1, "v1"))
|
||||
require.NoError(t, dashboardModule.ReconcileSystemDashboards(ctx, orgID))
|
||||
|
||||
provisioned, err := dashboardModule.GetSystemDashboard(ctx, orgID, testDashboardName)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = dashboardModule.UpdateV2(ctx, orgID, provisioned.ID, "user@signoz.io", newTestDefinition(t, 1, "edited").ToUpdatable())
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "cannot be modified")
|
||||
}
|
||||
|
||||
func TestReconcileDoesNotDowngrade(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
sqlStore := newTestSQLStore(t)
|
||||
orgID := valuer.GenerateUUID()
|
||||
|
||||
newerModule := newTestModule(t, sqlStore, newTestDefinition(t, 3, "v3"))
|
||||
require.NoError(t, newerModule.ReconcileSystemDashboards(ctx, orgID))
|
||||
|
||||
olderModule := newTestModule(t, sqlStore, newTestDefinition(t, 2, "v2"))
|
||||
require.NoError(t, olderModule.ReconcileSystemDashboards(ctx, orgID))
|
||||
|
||||
got, err := newerModule.GetSystemDashboard(ctx, orgID, testDashboardName)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "v3", got.Spec.Display.Name)
|
||||
assert.Equal(t, 3, stateVersion(t, newerModule, ctx, orgID))
|
||||
}
|
||||
|
||||
func TestGetRejectsANonSystemDashboard(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
sqlStore := newTestSQLStore(t)
|
||||
orgID := valuer.GenerateUUID()
|
||||
|
||||
dashboardModule := newTestModule(t, sqlStore)
|
||||
|
||||
var postable dashboardtypes.PostableDashboardV2
|
||||
require.NoError(t, postable.UnmarshalJSON([]byte(`{
|
||||
"schemaVersion": "`+dashboardtypes.SchemaVersion+`",
|
||||
"name": "a-user-dashboard",
|
||||
"tags": [],
|
||||
"spec": {"display": {"name": "user"}, "variables": [], "panels": {}, "layouts": []}
|
||||
}`)))
|
||||
_, err := dashboardModule.CreateV2(ctx, orgID, "user@signoz.io", valuer.GenerateUUID(), dashboardtypes.SourceUser, postable)
|
||||
require.NoError(t, err)
|
||||
|
||||
// The server-side prefix makes user names structurally unreachable here.
|
||||
_, err = dashboardModule.GetSystemDashboard(ctx, orgID, "a-user-dashboard")
|
||||
require.Error(t, err)
|
||||
|
||||
_, err = dashboardModule.GetSystemDashboard(ctx, orgID, dashboardtypes.SystemDashboardNamePrefix+testDashboardName)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "must not carry")
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/alertmanager"
|
||||
"github.com/SigNoz/signoz/pkg/modules/dashboard"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||
"github.com/SigNoz/signoz/pkg/modules/quickfilter"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
@@ -15,11 +14,10 @@ type setter struct {
|
||||
store types.OrganizationStore
|
||||
alertmanager alertmanager.Alertmanager
|
||||
quickfilter quickfilter.Module
|
||||
dashboard dashboard.Module
|
||||
}
|
||||
|
||||
func NewSetter(store types.OrganizationStore, alertmanager alertmanager.Alertmanager, quickfilter quickfilter.Module, dashboard dashboard.Module) organization.Setter {
|
||||
return &setter{store: store, alertmanager: alertmanager, quickfilter: quickfilter, dashboard: dashboard}
|
||||
func NewSetter(store types.OrganizationStore, alertmanager alertmanager.Alertmanager, quickfilter quickfilter.Module) organization.Setter {
|
||||
return &setter{store: store, alertmanager: alertmanager, quickfilter: quickfilter}
|
||||
}
|
||||
|
||||
func (module *setter) Create(ctx context.Context, organization *types.Organization, createManagedRoles func(context.Context, valuer.UUID) error) error {
|
||||
@@ -39,10 +37,6 @@ func (module *setter) Create(ctx context.Context, organization *types.Organizati
|
||||
return err
|
||||
}
|
||||
|
||||
if err := module.dashboard.ReconcileSystemDashboards(ctx, organization.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -73,8 +73,8 @@ func (c *captureQuerier) LabelNames(context.Context, *storage.LabelHints, ...*la
|
||||
}
|
||||
|
||||
// metricNamesFromMatchers extracts the statically known metric name, if any.
|
||||
// The live path derives names from the matched series; the capture path has
|
||||
// no execution results, so only a __name__ equality contributes.
|
||||
// Only a __name__ equality contributes; a regex selector needs a series
|
||||
// lookup to learn the concrete names.
|
||||
func metricNamesFromMatchers(matchers []*labels.Matcher) []string {
|
||||
for _, m := range matchers {
|
||||
if m.Name == metricNameLabel && m.Type == labels.MatchEqual && m.Value != "" {
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"math"
|
||||
"slices"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/prometheus"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrystore"
|
||||
@@ -29,6 +30,7 @@ type client struct {
|
||||
settings factory.ScopedProviderSettings
|
||||
telemetryStore telemetrystore.TelemetryStore
|
||||
lookbackMs int64
|
||||
cfg prometheus.ClickhouseV2Config
|
||||
}
|
||||
|
||||
func newClient(settings factory.ScopedProviderSettings, telemetryStore telemetrystore.TelemetryStore, cfg prometheus.Config) *client {
|
||||
@@ -41,6 +43,7 @@ func newClient(settings factory.ScopedProviderSettings, telemetryStore telemetry
|
||||
settings: settings,
|
||||
telemetryStore: telemetryStore,
|
||||
lookbackMs: lookback.Milliseconds(),
|
||||
cfg: cfg.ClickhouseV2,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,6 +80,13 @@ func (c *client) selectSeries(ctx context.Context, query string, args []any) (*s
|
||||
if name := lset.Get(metricNameLabel); name != "" {
|
||||
names[name] = struct{}{}
|
||||
}
|
||||
if c.cfg.MaxFetchedSeries > 0 && len(lookup.fingerprints) > c.cfg.MaxFetchedSeries {
|
||||
return nil, errors.NewInvalidInputf(
|
||||
errors.CodeInvalidInput,
|
||||
"promql selector matched more than %d series; narrow the label matchers or raise prometheus::clickhousev2::max_fetched_series",
|
||||
c.cfg.MaxFetchedSeries,
|
||||
)
|
||||
}
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
@@ -136,6 +146,8 @@ func (c *client) selectSamples(ctx context.Context, query string, args []any, lo
|
||||
first = true
|
||||
haveCurrent bool
|
||||
staleMarker = math.Float64frombits(promValue.StaleNaN)
|
||||
maxSamples = c.cfg.MaxFetchedSamples
|
||||
fetched int64
|
||||
unknownCount int
|
||||
)
|
||||
|
||||
@@ -144,6 +156,15 @@ func (c *client) selectSamples(ctx context.Context, query string, args []any, lo
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fetched++
|
||||
if maxSamples > 0 && fetched > maxSamples {
|
||||
return nil, errors.NewInvalidInputf(
|
||||
errors.CodeInvalidInput,
|
||||
"promql query would fetch more than %d samples; narrow the selector or time range, or raise prometheus::clickhousev2::max_fetched_samples",
|
||||
maxSamples,
|
||||
)
|
||||
}
|
||||
|
||||
if first || fingerprint != prevFp {
|
||||
first = false
|
||||
prevFp = fingerprint
|
||||
|
||||
54
pkg/prometheus/clickhouseprometheusv2/client_test.go
Normal file
54
pkg/prometheus/clickhouseprometheusv2/client_test.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package clickhouseprometheusv2
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
cmock "github.com/SigNoz/clickhouse-go-mock"
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/prometheus/prometheus/model/labels"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var seriesCols = []cmock.ColumnType{
|
||||
{Name: "fingerprint", Type: "UInt64"},
|
||||
{Name: "labels", Type: "String"},
|
||||
}
|
||||
|
||||
var samplesCols = []cmock.ColumnType{
|
||||
{Name: "fingerprint", Type: "UInt64"},
|
||||
{Name: "unix_milli", Type: "Int64"},
|
||||
{Name: "value", Type: "Float64"},
|
||||
{Name: "flags", Type: "UInt32"},
|
||||
}
|
||||
|
||||
func TestSelectSeriesBudget(t *testing.T) {
|
||||
c, store := newTestClient(t)
|
||||
c.cfg.MaxFetchedSeries = 1
|
||||
|
||||
store.Mock().ExpectQuery("SELECT fingerprint, any\\(labels\\)").WillReturnRows(cmock.NewRows(seriesCols, [][]any{
|
||||
{uint64(1), `{"__name__":"up","instance":"a"}`},
|
||||
{uint64(2), `{"__name__":"up","instance":"b"}`},
|
||||
}))
|
||||
|
||||
_, err := c.selectSeries(context.Background(), "SELECT fingerprint, any(labels) FROM t", nil)
|
||||
require.Error(t, err)
|
||||
assert.True(t, errors.Ast(err, errors.TypeInvalidInput), "budget refusal must be typed invalid input, got %v", err)
|
||||
}
|
||||
|
||||
func TestSelectSamplesBudget(t *testing.T) {
|
||||
c, store := newTestClient(t)
|
||||
c.cfg.MaxFetchedSamples = 2
|
||||
|
||||
store.Mock().ExpectQuery("SELECT fingerprint, unix_milli").WillReturnRows(cmock.NewRows(samplesCols, [][]any{
|
||||
{uint64(1), int64(1_700_000_000_000), 1.0, uint32(0)},
|
||||
{uint64(1), int64(1_700_000_060_000), 2.0, uint32(0)},
|
||||
{uint64(1), int64(1_700_000_120_000), 3.0, uint32(0)},
|
||||
}))
|
||||
|
||||
lookup := &seriesLookup{fingerprints: map[uint64]labels.Labels{1: labels.FromStrings("__name__", "up")}}
|
||||
_, err := c.selectSamples(context.Background(), "SELECT fingerprint, unix_milli, value, flags FROM t", nil, lookup)
|
||||
require.Error(t, err)
|
||||
assert.True(t, errors.Ast(err, errors.TypeInvalidInput), "budget refusal must be typed invalid input, got %v", err)
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"math"
|
||||
"sort"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
@@ -88,12 +89,17 @@ func (e *executor) TryExecuteRange(ctx context.Context, qs string, start, end ti
|
||||
}
|
||||
|
||||
// Evaluate every unit concurrently on its own grid (the query grid, or a
|
||||
// subquery grid); each is one series lookup plus one grid query.
|
||||
// subquery grid); each is one grid query (see executeUnit for when a
|
||||
// series lookup precedes it). The units share one grid-cell budget:
|
||||
// transpiled results never pass through the engine's sample limiter, so
|
||||
// without it a large series-count x grid-width query would buffer
|
||||
// unbounded arrays — the OOM this provider exists to prevent.
|
||||
results := make([][]transpiledSeries, len(plan.units))
|
||||
var gridCells atomic.Int64
|
||||
eg, egCtx := errgroup.WithContext(ctx)
|
||||
for i, unit := range plan.units {
|
||||
eg.Go(func() error {
|
||||
res, err := e.executeUnit(egCtx, &unit.core, unit.grid)
|
||||
res, err := e.executeUnit(egCtx, &unit.core, unit.grid, &gridCells)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -133,7 +139,7 @@ type transpiledSeries struct {
|
||||
values []*float64
|
||||
}
|
||||
|
||||
func (e *executor) executeUnit(ctx context.Context, unit *coreUnit, grid gridContext) ([]transpiledSeries, error) {
|
||||
func (e *executor) executeUnit(ctx context.Context, unit *coreUnit, grid gridContext, gridCells *atomic.Int64) ([]transpiledSeries, error) {
|
||||
startMs, endMs, stepMs := grid.startMs, grid.endMs, grid.stepMs
|
||||
windowMs := unit.rangeMs
|
||||
if unit.kind == unitInstant {
|
||||
@@ -142,19 +148,27 @@ func (e *executor) executeUnit(ctx context.Context, unit *coreUnit, grid gridCon
|
||||
dataStart := startMs - unit.offsetMs - windowMs
|
||||
dataEnd := endMs - unit.offsetMs
|
||||
|
||||
seriesQuery, seriesArgs, err := buildSeriesQuery(dataStart, dataEnd, unit.matchers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
lookup, err := e.client.selectSeries(ctx, seriesQuery, seriesArgs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(lookup.fingerprints) == 0 {
|
||||
return nil, nil
|
||||
// The group-key join resolves the matchers on its own, so the unit
|
||||
// statement only needs concrete metric names for the samples
|
||||
// primary-key prefix. A selector without a static __name__ learns them
|
||||
// through the series lookup; every other selector skips the roundtrip.
|
||||
metricNames := metricNamesFromMatchers(unit.matchers)
|
||||
if metricNames == nil {
|
||||
seriesQuery, seriesArgs, err := buildSeriesQuery(dataStart, dataEnd, unit.matchers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
lookup, err := e.client.selectSeries(ctx, seriesQuery, seriesArgs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(lookup.fingerprints) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
metricNames = lookup.metricNames
|
||||
}
|
||||
|
||||
query, args, err := buildUnitSQL(unit, lookup.metricNames, dataStart, dataEnd, startMs, endMs, stepMs, e.client.lookbackMs)
|
||||
query, args, err := buildUnitSQL(unit, metricNames, dataStart, dataEnd, startMs, endMs, stepMs, e.client.lookbackMs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -190,6 +204,17 @@ func (e *executor) executeUnit(ctx context.Context, unit *coreUnit, grid gridCon
|
||||
if err := rows.Scan(targets...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// One row buffers one grid array; series count times grid width is
|
||||
// the transpiled equivalent of fetched samples. Counted per row as
|
||||
// the arrays accumulate: without a series lookup there is no series
|
||||
// count to charge up front.
|
||||
if maxSamples := e.client.cfg.MaxFetchedSamples; maxSamples > 0 && gridCells.Add(int64(len(gridValues))) > maxSamples {
|
||||
return nil, errors.NewInvalidInputf(
|
||||
errors.CodeInvalidInput,
|
||||
"promql query would buffer more than %d output points; narrow the selector or time range, or raise prometheus::clickhousev2::max_fetched_samples",
|
||||
maxSamples,
|
||||
)
|
||||
}
|
||||
var lset labels.Labels
|
||||
if keyNames != nil {
|
||||
builder := labels.NewScratchBuilder(len(keyNames))
|
||||
|
||||
@@ -2,6 +2,7 @@ package clickhouseprometheusv2
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -27,9 +28,15 @@ func newTestClient(t *testing.T) (*client, *telemetrystoretest.Provider) {
|
||||
return newClient(settings, store, prometheus.Config{}), store
|
||||
}
|
||||
|
||||
var seriesCols = []cmock.ColumnType{
|
||||
{Name: "fingerprint", Type: "UInt64"},
|
||||
{Name: "labels", Type: "String"},
|
||||
var unitCols = []cmock.ColumnType{
|
||||
{Name: "gkey", Type: "String"},
|
||||
{Name: "grid", Type: "Array(Nullable(Float64))"},
|
||||
}
|
||||
|
||||
// anyArgs matches a bound-argument list by count alone: the mock treats a
|
||||
// nil expected argument as a wildcard.
|
||||
func anyArgs(n int) []any {
|
||||
return make([]any, n)
|
||||
}
|
||||
|
||||
func parse(t *testing.T, q string) parser.Expr {
|
||||
@@ -534,6 +541,30 @@ func TestDisjointWindowLattice(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Transpiled results never pass the engine's sample limiter, so the grid
|
||||
// cells (series x grid width) must be budgeted as the arrays accumulate —
|
||||
// otherwise a wide query rebuilds the OOM this provider exists to prevent.
|
||||
func TestExecuteUnit_GridCellBudget(t *testing.T) {
|
||||
c, store := newTestClient(t)
|
||||
c.cfg.MaxFetchedSamples = 100
|
||||
e := &executor{client: c, parser: prometheus.NewParser()}
|
||||
|
||||
grid61 := make([]*float64, 61)
|
||||
store.Mock().ExpectQuery("timeSeriesRateToGrid").WithArgs(anyArgs(7)...).WillReturnRows(cmock.NewRows(
|
||||
[]cmock.ColumnType{{Name: "g0", Type: "String"}, {Name: "grid", Type: "Array(Nullable(Float64))"}},
|
||||
[][]any{{"api", grid61}, {"web", grid61}},
|
||||
))
|
||||
|
||||
plan, ok := classify(parse(t, `sum by (job) (rate(up[5m]))`), gridContext{startMs: 1_700_000_000_000, endMs: 1_700_003_600_000, stepMs: 60_000})
|
||||
require.True(t, ok)
|
||||
|
||||
// 2 series x 61 grid points = 122 cells > 100.
|
||||
var cells atomic.Int64
|
||||
_, err := e.executeUnit(context.Background(), &plan.units[0].core, plan.units[0].grid, &cells)
|
||||
require.Error(t, err)
|
||||
assert.True(t, errors.Ast(err, errors.TypeInvalidInput), "budget refusal must be typed invalid input, got %v", err)
|
||||
}
|
||||
|
||||
func TestTryExecuteRange_WindowedGateFallsBack(t *testing.T) {
|
||||
c, store := newTestClient(t)
|
||||
e := &executor{client: c, parser: prometheus.NewParser()}
|
||||
@@ -553,7 +584,7 @@ func TestTryExecuteRange_WindowedGateFallsBack(t *testing.T) {
|
||||
|
||||
// 1m range at 5m step: the windows are disjoint slivers — no
|
||||
// divisibility or width requirement, so this transpiles.
|
||||
store.Mock().ExpectQuery("SELECT fingerprint, any\\(labels\\)").WithArgs("up", int64(1_699_999_200_000), int64(1_700_003_600_000)).WillReturnRows(cmock.NewRows(seriesCols, [][]any{}))
|
||||
store.Mock().ExpectQuery("FROM signoz_metrics\\.distributed_samples_v4").WithArgs(anyArgs(9)...).WillReturnRows(cmock.NewRows(unitCols, [][]any{}))
|
||||
_, ok, err = e.TryExecuteRange(context.Background(), `avg_over_time(up[1m])`, start, end, 5*time.Minute)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, ok, "range below step is the disjoint form and must transpile")
|
||||
@@ -637,12 +668,12 @@ func TestTryExecuteRange_LastStyleWindowBelowStepTranspiles(t *testing.T) {
|
||||
start := time.UnixMilli(1_700_000_000_000)
|
||||
end := time.UnixMilli(1_700_003_600_000)
|
||||
|
||||
store.Mock().ExpectQuery("SELECT fingerprint, any\\(labels\\)").WithArgs("up", int64(1_699_999_200_000), int64(1_700_003_600_000)).WillReturnRows(cmock.NewRows(seriesCols, [][]any{}))
|
||||
store.Mock().ExpectQuery("timeSeriesLastToGrid").WithArgs(anyArgs(10)...).WillReturnRows(cmock.NewRows(unitCols, [][]any{}))
|
||||
_, ok, err := e.TryExecuteRange(context.Background(), `sum by (pod) (up)`, start, end, time.Hour)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, ok, "instant selection at step > lookback must transpile")
|
||||
|
||||
store.Mock().ExpectQuery("SELECT fingerprint, any\\(labels\\)").WithArgs("up", int64(1_699_999_200_000), int64(1_700_003_600_000)).WillReturnRows(cmock.NewRows(seriesCols, [][]any{}))
|
||||
store.Mock().ExpectQuery("timeSeriesLastToGrid").WithArgs(anyArgs(9)...).WillReturnRows(cmock.NewRows(unitCols, [][]any{}))
|
||||
_, ok, err = e.TryExecuteRange(context.Background(), `last_over_time(up[10m])`, start, end, time.Hour)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, ok, "last_over_time at range < step must transpile")
|
||||
|
||||
@@ -13,6 +13,16 @@ type ActiveQueryTrackerConfig struct {
|
||||
MaxConcurrent int `mapstructure:"max_concurrent"`
|
||||
}
|
||||
|
||||
type ClickhouseV2Config struct {
|
||||
// MaxFetchedSeries caps the series one selector may match; 0 disables
|
||||
// the cap.
|
||||
MaxFetchedSeries int `mapstructure:"max_fetched_series"`
|
||||
|
||||
// MaxFetchedSamples caps the samples (engine path) or buffered grid
|
||||
// cells (transpiled path) one query may fetch; 0 disables the cap.
|
||||
MaxFetchedSamples int64 `mapstructure:"max_fetched_samples"`
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
ActiveQueryTrackerConfig ActiveQueryTrackerConfig `mapstructure:"active_query_tracker"`
|
||||
|
||||
@@ -28,6 +38,8 @@ type Config struct {
|
||||
// ProviderName selects the storage provider: "clickhouse" (default) or
|
||||
// "clickhousev2".
|
||||
ProviderName string `mapstructure:"provider"`
|
||||
|
||||
ClickhouseV2 ClickhouseV2Config `mapstructure:"clickhousev2"`
|
||||
}
|
||||
|
||||
func NewConfigFactory() factory.ConfigFactory {
|
||||
@@ -43,6 +55,10 @@ func newConfig() factory.Config {
|
||||
},
|
||||
Timeout: 2 * time.Minute,
|
||||
ProviderName: "clickhouse",
|
||||
ClickhouseV2: ClickhouseV2Config{
|
||||
MaxFetchedSeries: 500_000,
|
||||
MaxFetchedSamples: 50_000_000,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +69,9 @@ func (c Config) Validate() error {
|
||||
if c.ProviderName != "" && c.ProviderName != "clickhouse" && c.ProviderName != "clickhousev2" {
|
||||
return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "prometheus::provider must be one of [clickhouse, clickhousev2], got %q", c.ProviderName)
|
||||
}
|
||||
if c.ClickhouseV2.MaxFetchedSeries < 0 || c.ClickhouseV2.MaxFetchedSamples < 0 {
|
||||
return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "prometheus::clickhousev2 limits must not be negative")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
30
pkg/prometheus/errors.go
Normal file
30
pkg/prometheus/errors.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package prometheus
|
||||
|
||||
import (
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/prometheus/prometheus/promql"
|
||||
)
|
||||
|
||||
// TypedStorageError walks an engine execution error chain looking for a
|
||||
// SigNoz-typed invalid-input error raised by the storage layer (the fetch
|
||||
// budget refusals). Every wrapper level is stepped through by hand: Ast is a
|
||||
// bare type cast, not an unwrap — it misses a typed error behind the
|
||||
// engine's "expanding series: %w" — and promql.ErrStorage has no Unwrap
|
||||
// method at all, so a plain unwrap loop would stop at it.
|
||||
func TypedStorageError(execErr error) error {
|
||||
for e := execErr; e != nil; {
|
||||
if errors.Ast(e, errors.TypeInvalidInput) {
|
||||
return e
|
||||
}
|
||||
if es, ok := e.(promql.ErrStorage); ok {
|
||||
e = es.Err
|
||||
continue
|
||||
}
|
||||
u, ok := e.(interface{ Unwrap() error })
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
e = u.Unwrap()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
22
pkg/prometheus/errors_test.go
Normal file
22
pkg/prometheus/errors_test.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package prometheus
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/prometheus/prometheus/promql"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestTypedStorageError(t *testing.T) {
|
||||
budget := errors.NewInvalidInputf(errors.CodeInvalidInput, "too many series")
|
||||
|
||||
// The engine wraps a storage error as expanding series: %w inside
|
||||
// promql.ErrStorage, which has no Unwrap method.
|
||||
wrapped := promql.ErrStorage{Err: fmt.Errorf("expanding series: %w", budget)}
|
||||
assert.Equal(t, budget, TypedStorageError(wrapped))
|
||||
|
||||
assert.Nil(t, TypedStorageError(promql.ErrStorage{Err: fmt.Errorf("connection refused")}))
|
||||
assert.Nil(t, TypedStorageError(nil))
|
||||
}
|
||||
218
pkg/prometheus/handler.go
Normal file
218
pkg/prometheus/handler.go
Normal file
@@ -0,0 +1,218 @@
|
||||
package prometheus
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"math"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
promModel "github.com/prometheus/common/model"
|
||||
"github.com/prometheus/prometheus/promql"
|
||||
"github.com/prometheus/prometheus/util/stats"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
)
|
||||
|
||||
// Handler serves the Prometheus HTTP query API over a Prometheus provider:
|
||||
// /query and /query_range in the shape of Prometheus' /api/v1 endpoints
|
||||
// (https://prometheus.io/docs/prometheus/latest/querying/api/), intended to
|
||||
// be mounted under a distinguishing prefix (/prometheus/api/v1) so
|
||||
// PromQL-only endpoints are separate from the SigNoz query APIs. The request
|
||||
// and response contracts follow Prometheus: form-encoded GET/POST params,
|
||||
// {"status":"success","data":{resultType,result}} on success and
|
||||
// {"status":"error","errorType","error"} with Prometheus' status codes on
|
||||
// failure — so Prometheus-compatible clients can point at the prefix. The
|
||||
// wire shapes are documented as OpenAPI schemas in render.go.
|
||||
type Handler interface {
|
||||
Query(http.ResponseWriter, *http.Request)
|
||||
|
||||
QueryRange(http.ResponseWriter, *http.Request)
|
||||
}
|
||||
|
||||
type handler struct {
|
||||
logger *slog.Logger
|
||||
prom Prometheus
|
||||
}
|
||||
|
||||
func NewHandler(logger *slog.Logger, prom Prometheus) Handler {
|
||||
return &handler{logger: logger, prom: prom}
|
||||
}
|
||||
|
||||
// QueryRange evaluates an expression over a grid: query, start, end, step,
|
||||
// and optional timeout/stats params, all in Prometheus' formats.
|
||||
func (h *handler) QueryRange(w http.ResponseWriter, r *http.Request) {
|
||||
start, err := parseTime(r.FormValue("start"))
|
||||
if err != nil {
|
||||
h.respondError(r.Context(), w, errBadData, err)
|
||||
return
|
||||
}
|
||||
end, err := parseTime(r.FormValue("end"))
|
||||
if err != nil {
|
||||
h.respondError(r.Context(), w, errBadData, err)
|
||||
return
|
||||
}
|
||||
if end.Before(start) {
|
||||
h.respondError(r.Context(), w, errBadData, errors.NewInvalidInputf(errors.CodeInvalidInput, "end timestamp must not be before start time"))
|
||||
return
|
||||
}
|
||||
step, err := parseDuration(r.FormValue("step"))
|
||||
if err != nil {
|
||||
h.respondError(r.Context(), w, errBadData, err)
|
||||
return
|
||||
}
|
||||
if step <= 0 {
|
||||
h.respondError(r.Context(), w, errBadData, errors.NewInvalidInputf(errors.CodeInvalidInput, "zero or negative query resolution step widths are not accepted. Try a positive integer"))
|
||||
return
|
||||
}
|
||||
// The engine materializes every point of every series; an unbounded
|
||||
// grid is an unbounded allocation. 11,000 points covers 60s resolution
|
||||
// for a week or 1h resolution for a year.
|
||||
if end.Sub(start)/step > 11000 {
|
||||
h.respondError(r.Context(), w, errBadData, errors.NewInvalidInputf(errors.CodeInvalidInput, "exceeded maximum resolution of 11,000 points per timeseries. Try decreasing the query resolution (?step=XX)"))
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel, err := h.contextWithTimeout(r)
|
||||
if err != nil {
|
||||
h.respondError(r.Context(), w, errBadData, err)
|
||||
return
|
||||
}
|
||||
defer cancel()
|
||||
|
||||
if h.tryRangeExecutor(ctx, w, r, start, end, step) {
|
||||
return
|
||||
}
|
||||
|
||||
qry, err := h.prom.Engine().NewRangeQuery(ctx, h.prom.Storage(), nil, r.FormValue("query"), start, end, step)
|
||||
if err != nil {
|
||||
h.respondError(r.Context(), w, errBadData, err)
|
||||
return
|
||||
}
|
||||
h.exec(ctx, w, r, qry)
|
||||
}
|
||||
|
||||
// tryRangeExecutor serves the query the way a RangeExecutor provider is
|
||||
// designed to serve: evaluated inside the datastore when the shape allows.
|
||||
// It reports whether the response was written.
|
||||
func (h *handler) tryRangeExecutor(ctx context.Context, w http.ResponseWriter, r *http.Request, start, end time.Time, step time.Duration) bool {
|
||||
re, ok := h.prom.(RangeExecutor)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
matrix, served, err := re.TryExecuteRange(ctx, r.FormValue("query"), start, end, step)
|
||||
if err != nil {
|
||||
h.respondError(ctx, w, errExec, err)
|
||||
return true
|
||||
}
|
||||
if !served {
|
||||
return false
|
||||
}
|
||||
h.respond(ctx, w, &queryData{ResultType: matrix.Type(), Result: matrix}, nil, nil)
|
||||
return true
|
||||
}
|
||||
|
||||
// Query evaluates an expression at a single instant: query and optional
|
||||
// time/timeout/stats params. A missing time evaluates at the server's now,
|
||||
// as in Prometheus.
|
||||
func (h *handler) Query(w http.ResponseWriter, r *http.Request) {
|
||||
ts := time.Now()
|
||||
if t := r.FormValue("time"); t != "" {
|
||||
var err error
|
||||
ts, err = parseTime(t)
|
||||
if err != nil {
|
||||
h.respondError(r.Context(), w, errBadData, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ctx, cancel, err := h.contextWithTimeout(r)
|
||||
if err != nil {
|
||||
h.respondError(r.Context(), w, errBadData, err)
|
||||
return
|
||||
}
|
||||
defer cancel()
|
||||
|
||||
qry, err := h.prom.Engine().NewInstantQuery(ctx, h.prom.Storage(), nil, r.FormValue("query"), ts)
|
||||
if err != nil {
|
||||
h.respondError(r.Context(), w, errBadData, err)
|
||||
return
|
||||
}
|
||||
h.exec(ctx, w, r, qry)
|
||||
}
|
||||
|
||||
func (h *handler) exec(ctx context.Context, w http.ResponseWriter, r *http.Request, qry promql.Query) {
|
||||
defer qry.Close()
|
||||
res := qry.Exec(ctx)
|
||||
if res.Err != nil {
|
||||
h.logger.ErrorContext(ctx, "error evaluating promql query", errors.Attr(res.Err))
|
||||
switch res.Err.(type) {
|
||||
case promql.ErrQueryCanceled:
|
||||
h.respondError(ctx, w, errCanceled, res.Err)
|
||||
case promql.ErrQueryTimeout:
|
||||
h.respondError(ctx, w, errTimeout, res.Err)
|
||||
case promql.ErrStorage:
|
||||
// A fetch-budget refusal is the storage-level twin of the
|
||||
// engine's own too-many-samples error, which upstream maps to
|
||||
// "execution", not "internal".
|
||||
if typed := TypedStorageError(res.Err); typed != nil {
|
||||
h.respondError(ctx, w, errExec, typed)
|
||||
return
|
||||
}
|
||||
h.respondError(ctx, w, errInternal, res.Err)
|
||||
default:
|
||||
h.respondError(ctx, w, errExec, res.Err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
data := &queryData{ResultType: res.Value.Type(), Result: res.Value}
|
||||
if r.FormValue("stats") != "" {
|
||||
data.Stats = stats.NewQueryStats(qry.Stats())
|
||||
}
|
||||
warnings, infos := res.Warnings.AsStrings(r.FormValue("query"), 10, 10)
|
||||
h.respond(ctx, w, data, warnings, infos)
|
||||
}
|
||||
|
||||
func (h *handler) contextWithTimeout(r *http.Request) (context.Context, context.CancelFunc, error) {
|
||||
ctx := r.Context()
|
||||
if to := r.FormValue("timeout"); to != "" {
|
||||
timeout, err := parseDuration(to)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(ctx, timeout)
|
||||
return ctx, cancel, nil
|
||||
}
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
return ctx, cancel, nil
|
||||
}
|
||||
|
||||
// parseTime accepts Prometheus' time formats: float unix seconds or RFC3339.
|
||||
func parseTime(s string) (time.Time, error) {
|
||||
if t, err := strconv.ParseFloat(s, 64); err == nil {
|
||||
sec, ns := math.Modf(t)
|
||||
return time.Unix(int64(sec), int64(ns*float64(time.Second))), nil
|
||||
}
|
||||
if t, err := time.Parse(time.RFC3339Nano, s); err == nil {
|
||||
return t, nil
|
||||
}
|
||||
return time.Time{}, errors.NewInvalidInputf(errors.CodeInvalidInput, "cannot parse %q to a valid timestamp", s)
|
||||
}
|
||||
|
||||
// parseDuration accepts Prometheus' duration formats: float seconds or a
|
||||
// duration string like 5m.
|
||||
func parseDuration(s string) (time.Duration, error) {
|
||||
if d, err := strconv.ParseFloat(s, 64); err == nil {
|
||||
ts := d * float64(time.Second)
|
||||
if ts > float64(math.MaxInt64) || ts < float64(math.MinInt64) {
|
||||
return 0, errors.NewInvalidInputf(errors.CodeInvalidInput, "cannot parse %q to a valid duration. It overflows int64", s)
|
||||
}
|
||||
return time.Duration(ts), nil
|
||||
}
|
||||
if d, err := promModel.ParseDuration(s); err == nil {
|
||||
return time.Duration(d), nil
|
||||
}
|
||||
return 0, errors.NewInvalidInputf(errors.CodeInvalidInput, "cannot parse %q to a valid duration", s)
|
||||
}
|
||||
164
pkg/prometheus/render.go
Normal file
164
pkg/prometheus/render.go
Normal file
@@ -0,0 +1,164 @@
|
||||
package prometheus
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/prometheus/prometheus/promql/parser"
|
||||
"github.com/prometheus/prometheus/util/stats"
|
||||
"github.com/swaggest/jsonschema-go"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
)
|
||||
|
||||
// This file is the single description of the Prometheus API wire shapes:
|
||||
// the runtime envelope the handler encodes, and the *Schema types that
|
||||
// document the same shapes in the generated OpenAPI spec. The contract is
|
||||
// upstream's (https://prometheus.io/docs/prometheus/latest/querying/api/);
|
||||
// the schemas describe it, they do not define it.
|
||||
|
||||
type errorType string
|
||||
|
||||
const (
|
||||
errBadData errorType = "bad_data"
|
||||
errExec errorType = "execution"
|
||||
errCanceled errorType = "canceled"
|
||||
errTimeout errorType = "timeout"
|
||||
errInternal errorType = "internal"
|
||||
)
|
||||
|
||||
type queryData struct {
|
||||
ResultType parser.ValueType `json:"resultType"`
|
||||
Result parser.Value `json:"result"`
|
||||
Stats stats.QueryStats `json:"stats,omitempty"`
|
||||
}
|
||||
|
||||
type response struct {
|
||||
Status string `json:"status"`
|
||||
Data *queryData `json:"data,omitempty"`
|
||||
ErrorType errorType `json:"errorType,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Warnings []string `json:"warnings,omitempty"`
|
||||
Infos []string `json:"infos,omitempty"`
|
||||
}
|
||||
|
||||
func (h *handler) respond(ctx context.Context, w http.ResponseWriter, data *queryData, warnings, infos []string) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
if err := json.NewEncoder(w).Encode(&response{Status: "success", Data: data, Warnings: warnings, Infos: infos}); err != nil {
|
||||
h.logger.ErrorContext(ctx, "error writing prometheus api response", errors.Attr(err))
|
||||
}
|
||||
}
|
||||
|
||||
// respondError follows Prometheus' status-code mapping: bad_data 400,
|
||||
// execution 422, canceled/timeout 503, internal 500.
|
||||
func (h *handler) respondError(ctx context.Context, w http.ResponseWriter, typ errorType, err error) {
|
||||
code := http.StatusInternalServerError
|
||||
switch typ {
|
||||
case errBadData:
|
||||
code = http.StatusBadRequest
|
||||
case errExec:
|
||||
code = http.StatusUnprocessableEntity
|
||||
case errCanceled, errTimeout:
|
||||
code = http.StatusServiceUnavailable
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(code)
|
||||
if encErr := json.NewEncoder(w).Encode(&response{Status: "error", ErrorType: typ, Error: err.Error()}); encErr != nil {
|
||||
h.logger.ErrorContext(ctx, "error writing prometheus api error response", errors.Attr(encErr))
|
||||
}
|
||||
}
|
||||
|
||||
// The endpoints accept parameters as URL query params or a form-encoded
|
||||
// body, on GET and POST alike.
|
||||
type QueryParamsSchema struct {
|
||||
Query string `query:"query" required:"true" description:"PromQL expression."`
|
||||
Time string `query:"time" description:"Evaluation timestamp: RFC3339 or float unix seconds. Defaults to the server's current time."`
|
||||
Timeout string `query:"timeout" description:"Evaluation timeout: duration string or float seconds."`
|
||||
Stats string `query:"stats" description:"Any non-empty value includes query statistics in the response."`
|
||||
}
|
||||
|
||||
type QueryRangeParamsSchema struct {
|
||||
Query string `query:"query" required:"true" description:"PromQL expression."`
|
||||
Start string `query:"start" required:"true" description:"Range start: RFC3339 or float unix seconds."`
|
||||
End string `query:"end" required:"true" description:"Range end: RFC3339 or float unix seconds."`
|
||||
Step string `query:"step" required:"true" description:"Resolution step: duration string or float seconds."`
|
||||
Timeout string `query:"timeout" description:"Evaluation timeout: duration string or float seconds."`
|
||||
Stats string `query:"stats" description:"Any non-empty value includes query statistics in the response."`
|
||||
}
|
||||
|
||||
type SuccessResponseSchema struct {
|
||||
Status string `json:"status" enum:"success" required:"true"`
|
||||
Data QueryDataSchema `json:"data" required:"true"`
|
||||
Warnings []string `json:"warnings,omitempty"`
|
||||
Infos []string `json:"infos,omitempty"`
|
||||
}
|
||||
|
||||
// QueryDataSchema is the result union, discriminated by resultType.
|
||||
type QueryDataSchema struct{}
|
||||
|
||||
var _ jsonschema.OneOfExposer = QueryDataSchema{}
|
||||
|
||||
func (QueryDataSchema) JSONSchemaOneOf() []interface{} {
|
||||
return []interface{}{MatrixDataSchema{}, VectorDataSchema{}, ScalarDataSchema{}, StringDataSchema{}}
|
||||
}
|
||||
|
||||
type MatrixDataSchema struct {
|
||||
ResultType string `json:"resultType" enum:"matrix" required:"true"`
|
||||
Result []MatrixSeriesSchema `json:"result" required:"true"`
|
||||
}
|
||||
|
||||
type MatrixSeriesSchema struct {
|
||||
Metric map[string]string `json:"metric" required:"true"`
|
||||
Values []SamplePairSchema `json:"values" required:"true"`
|
||||
}
|
||||
|
||||
type VectorDataSchema struct {
|
||||
ResultType string `json:"resultType" enum:"vector" required:"true"`
|
||||
Result []VectorSampleSchema `json:"result" required:"true"`
|
||||
}
|
||||
|
||||
type VectorSampleSchema struct {
|
||||
Metric map[string]string `json:"metric" required:"true"`
|
||||
Value SamplePairSchema `json:"value" required:"true"`
|
||||
}
|
||||
|
||||
type ScalarDataSchema struct {
|
||||
ResultType string `json:"resultType" enum:"scalar" required:"true"`
|
||||
Result SamplePairSchema `json:"result" required:"true"`
|
||||
}
|
||||
|
||||
type StringDataSchema struct {
|
||||
ResultType string `json:"resultType" enum:"string" required:"true"`
|
||||
Result SamplePairSchema `json:"result" required:"true"`
|
||||
}
|
||||
|
||||
// SamplePairSchema is the positional [timestamp, value] pair: a float of
|
||||
// unix seconds, then the value as a string ("NaN", "+Inf" and "-Inf"
|
||||
// included). Struct reflection cannot express a positional array, so the
|
||||
// schema is authored by hand.
|
||||
type SamplePairSchema struct{}
|
||||
|
||||
var _ jsonschema.Exposer = SamplePairSchema{}
|
||||
|
||||
func (SamplePairSchema) JSONSchema() (jsonschema.Schema, error) {
|
||||
item := jsonschema.Schema{}
|
||||
item.WithOneOf(
|
||||
(&jsonschema.Schema{}).WithType(jsonschema.Number.Type()).ToSchemaOrBool(),
|
||||
(&jsonschema.Schema{}).WithType(jsonschema.String.Type()).ToSchemaOrBool(),
|
||||
)
|
||||
s := jsonschema.Schema{}
|
||||
s.WithType(jsonschema.Array.Type())
|
||||
s.WithMinItems(2)
|
||||
s.WithMaxItems(2)
|
||||
s.WithItems(*(&jsonschema.Items{}).WithSchemaOrBool(item.ToSchemaOrBool()))
|
||||
s.WithDescription(`A [timestamp, value] pair: float unix seconds, then the string-encoded sample value ("NaN", "+Inf", "-Inf" included).`)
|
||||
return s, nil
|
||||
}
|
||||
|
||||
type ErrorResponseSchema struct {
|
||||
Status string `json:"status" enum:"error" required:"true"`
|
||||
ErrorType string `json:"errorType" enum:"bad_data,execution,canceled,timeout,internal" required:"true"`
|
||||
Error string `json:"error" required:"true"`
|
||||
}
|
||||
@@ -43,6 +43,10 @@ var quotedMetricOutsideBracesPattern = regexp.MustCompile(`"([^"]+)"\s*\{`)
|
||||
// tryEnhancePromQLExecError attempts to convert a PromQL execution error into
|
||||
// a properly typed error. Returns nil if the error is not a recognized execution error.
|
||||
func tryEnhancePromQLExecError(execErr error) error {
|
||||
if typed := prometheus.TypedStorageError(execErr); typed != nil {
|
||||
return typed
|
||||
}
|
||||
|
||||
var eqc promql.ErrQueryCanceled
|
||||
var eqt promql.ErrQueryTimeout
|
||||
var es promql.ErrStorage
|
||||
|
||||
@@ -387,6 +387,7 @@ func (aH *APIHandler) Respond(w http.ResponseWriter, data interface{}) {
|
||||
func (aH *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
|
||||
router.HandleFunc("/api/v1/query_range", am.ViewAccess(aH.queryRangeMetrics)).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/v1/query", am.ViewAccess(aH.queryMetrics)).Methods(http.MethodGet)
|
||||
|
||||
router.HandleFunc("/api/v1/rules", am.ViewAccess(aH.listRules)).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/v1/rules/{id}", am.ViewAccess(aH.getRule)).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/v1/rules", am.EditAccess(aH.createRule)).Methods(http.MethodPost)
|
||||
|
||||
@@ -75,6 +75,16 @@ func queryRangeVariables(body []byte) (map[string]qbtypes.VariableItem, error) {
|
||||
return variables, nil
|
||||
}
|
||||
|
||||
// PromQLResources is the resource set of a bare PromQL query: metrics on
|
||||
// the promql wildcard, the same ID resourcesForQuery assigns to a PromQL
|
||||
// query inside a composite — one grant covers both entry points.
|
||||
func PromQLResources(coretypes.ExtractorContext) ([]coretypes.ResourceWithID, error) {
|
||||
return []coretypes.ResourceWithID{{
|
||||
Resource: coretypes.ResourceTelemetryResourceMetrics,
|
||||
ID: qbtypes.QueryTypePromQL.StringValue() + "/" + coretypes.WildCardSelectorString,
|
||||
}}, nil
|
||||
}
|
||||
|
||||
func resourcesForQuery(query gjson.Result, variables map[string]qbtypes.VariableItem) ([]coretypes.ResourceWithID, error) {
|
||||
queryType := query.Get("type").String()
|
||||
typeWildcard := queryType + "/" + coretypes.WildCardSelectorString
|
||||
|
||||
@@ -50,6 +50,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/modules/tracedetail/impltracedetail"
|
||||
"github.com/SigNoz/signoz/pkg/modules/tracefunnel"
|
||||
"github.com/SigNoz/signoz/pkg/modules/tracefunnel/impltracefunnel"
|
||||
"github.com/SigNoz/signoz/pkg/prometheus"
|
||||
"github.com/SigNoz/signoz/pkg/querier"
|
||||
"github.com/SigNoz/signoz/pkg/ruler"
|
||||
"github.com/SigNoz/signoz/pkg/ruler/signozruler"
|
||||
@@ -84,6 +85,7 @@ type Handlers struct {
|
||||
RuleStateHistory rulestatehistory.Handler
|
||||
SpanMapperHandler spanmapper.Handler
|
||||
AlertmanagerHandler alertmanager.Handler
|
||||
PrometheusHandler prometheus.Handler
|
||||
TraceDetail tracedetail.Handler
|
||||
RulerHandler ruler.Handler
|
||||
LLMPricingRuleHandler llmpricingrule.Handler
|
||||
@@ -104,6 +106,7 @@ func NewHandlers(
|
||||
zeusService zeus.Zeus,
|
||||
registryHandler factory.Handler,
|
||||
alertmanagerService alertmanager.Alertmanager,
|
||||
prometheusService prometheus.Prometheus,
|
||||
rulerService ruler.Ruler,
|
||||
statsAggregator statsreporter.Aggregator,
|
||||
) Handlers {
|
||||
@@ -133,6 +136,7 @@ func NewHandlers(
|
||||
CloudIntegrationHandler: implcloudintegration.NewHandler(modules.CloudIntegration),
|
||||
SpanMapperHandler: implspanmapper.NewHandler(modules.SpanMapper),
|
||||
AlertmanagerHandler: signozalertmanager.NewHandler(alertmanagerService),
|
||||
PrometheusHandler: prometheus.NewHandler(providerSettings.Logger, prometheusService),
|
||||
TraceDetail: impltracedetail.NewHandler(modules.TraceDetail),
|
||||
RulerHandler: signozruler.NewHandler(rulerService),
|
||||
LLMPricingRuleHandler: impllmpricingrule.NewHandler(modules.LLMPricingRule),
|
||||
|
||||
@@ -49,9 +49,7 @@ func TestNewHandlers(t *testing.T) {
|
||||
queryParser := queryparser.New(providerSettings)
|
||||
require.NoError(t, err)
|
||||
tagModule := impltag.NewModule(impltag.NewStore(sqlstore))
|
||||
systemDashboardRegistry, err := impldashboard.NewSystemDashboardRegistry()
|
||||
require.NoError(t, err)
|
||||
dashboardModule := impldashboard.NewModule(impldashboard.NewStore(sqlstore), providerSettings, nil, orgGetter, queryParser, tagModule, systemDashboardRegistry)
|
||||
dashboardModule := impldashboard.NewModule(impldashboard.NewStore(sqlstore), providerSettings, nil, orgGetter, queryParser, tagModule)
|
||||
|
||||
flagger, err := flagger.New(context.Background(), instrumentationtest.New().ToProviderSettings(), flagger.Config{}, flagger.MustNewRegistry())
|
||||
require.NoError(t, err)
|
||||
@@ -65,7 +63,7 @@ func TestNewHandlers(t *testing.T) {
|
||||
|
||||
querierHandler := querier.NewHandler(providerSettings, nil, nil)
|
||||
registryHandler := factory.NewHandler(nil)
|
||||
handlers := NewHandlers(modules, providerSettings, nil, querierHandler, nil, nil, nil, nil, nil, nil, nil, registryHandler, alertmanager, nil, nil)
|
||||
handlers := NewHandlers(modules, providerSettings, nil, querierHandler, nil, nil, nil, nil, nil, nil, nil, registryHandler, alertmanager, nil, nil, nil)
|
||||
reflectVal := reflect.ValueOf(handlers)
|
||||
for i := 0; i < reflectVal.NumField(); i++ {
|
||||
f := reflectVal.Field(i)
|
||||
|
||||
@@ -67,35 +67,35 @@ import (
|
||||
)
|
||||
|
||||
type Modules struct {
|
||||
OrgGetter organization.Getter
|
||||
OrgSetter organization.Setter
|
||||
Preference preference.Module
|
||||
UserSetter user.Setter
|
||||
UserGetter user.Getter
|
||||
RetentionGetter retention.Getter
|
||||
SavedView savedview.Module
|
||||
Apdex apdex.Module
|
||||
Dashboard dashboard.Module
|
||||
QuickFilter quickfilter.Module
|
||||
TraceFunnel tracefunnel.Module
|
||||
RawDataExport rawdataexport.Module
|
||||
AuthDomain authdomain.Module
|
||||
Session session.Module
|
||||
Services services.Module
|
||||
SpanPercentile spanpercentile.Module
|
||||
MetricsExplorer metricsexplorer.Module
|
||||
MetricReductionRule metricreductionrule.Module
|
||||
InfraMonitoring inframonitoring.Module
|
||||
OrgGetter organization.Getter
|
||||
OrgSetter organization.Setter
|
||||
Preference preference.Module
|
||||
UserSetter user.Setter
|
||||
UserGetter user.Getter
|
||||
RetentionGetter retention.Getter
|
||||
SavedView savedview.Module
|
||||
Apdex apdex.Module
|
||||
Dashboard dashboard.Module
|
||||
QuickFilter quickfilter.Module
|
||||
TraceFunnel tracefunnel.Module
|
||||
RawDataExport rawdataexport.Module
|
||||
AuthDomain authdomain.Module
|
||||
Session session.Module
|
||||
Services services.Module
|
||||
SpanPercentile spanpercentile.Module
|
||||
MetricsExplorer metricsexplorer.Module
|
||||
MetricReductionRule metricreductionrule.Module
|
||||
InfraMonitoring inframonitoring.Module
|
||||
Promote promote.Module
|
||||
ServiceAccount serviceaccount.Module
|
||||
ServiceAccountGetter serviceaccount.Getter
|
||||
CloudIntegration cloudintegration.Module
|
||||
LogsPipeline logspipeline.Module
|
||||
RuleStateHistory rulestatehistory.Module
|
||||
TraceDetail tracedetail.Module
|
||||
SpanMapper spanmapper.Module
|
||||
LLMPricingRule llmpricingrule.Module
|
||||
Tag tag.Module
|
||||
LogsPipeline logspipeline.Module
|
||||
RuleStateHistory rulestatehistory.Module
|
||||
TraceDetail tracedetail.Module
|
||||
SpanMapper spanmapper.Module
|
||||
LLMPricingRule llmpricingrule.Module
|
||||
Tag tag.Module
|
||||
}
|
||||
|
||||
func NewModules(
|
||||
@@ -126,7 +126,7 @@ func NewModules(
|
||||
metricReductionRule metricreductionrule.Module,
|
||||
) Modules {
|
||||
quickfilter := implquickfilter.NewModule(implquickfilter.NewStore(sqlstore))
|
||||
orgSetter := implorganization.NewSetter(implorganization.NewStore(sqlstore), alertmanager, quickfilter, dashboard)
|
||||
orgSetter := implorganization.NewSetter(implorganization.NewStore(sqlstore), alertmanager, quickfilter)
|
||||
// Cleanup callbacks from other modules, invoked when a user is deleted.
|
||||
onDeleteUser := []user.OnDeleteUser{
|
||||
dashboard.DeletePreferencesForUser,
|
||||
@@ -136,34 +136,34 @@ func NewModules(
|
||||
authDomainModule := implauthdomain.NewModule(implauthdomain.NewStore(sqlstore), authNs, authz)
|
||||
|
||||
return Modules{
|
||||
OrgGetter: orgGetter,
|
||||
OrgSetter: orgSetter,
|
||||
Preference: implpreference.NewModule(implpreference.NewStore(sqlstore), preferencetypes.NewAvailablePreference()),
|
||||
SavedView: implsavedview.NewModule(implsavedview.NewStore(sqlstore)),
|
||||
Apdex: implapdex.NewModule(sqlstore),
|
||||
Dashboard: dashboard,
|
||||
UserSetter: userSetter,
|
||||
UserGetter: userGetter,
|
||||
RetentionGetter: retentionGetter,
|
||||
QuickFilter: quickfilter,
|
||||
TraceFunnel: impltracefunnel.NewModule(impltracefunnel.NewStore(sqlstore)),
|
||||
RawDataExport: implrawdataexport.NewModule(querier),
|
||||
AuthDomain: authDomainModule,
|
||||
Session: implsession.NewModule(providerSettings, authNs, userSetter, userGetter, authDomainModule, tokenizer, orgGetter, authz, config.Global),
|
||||
SpanPercentile: implspanpercentile.NewModule(querier, providerSettings),
|
||||
Services: implservices.NewModule(querier, telemetryStore),
|
||||
MetricsExplorer: implmetricsexplorer.NewModule(telemetryStore, telemetryMetadataStore, cache, ruleStore, dashboard, fl, providerSettings, config.MetricsExplorer),
|
||||
MetricReductionRule: metricReductionRule,
|
||||
InfraMonitoring: implinframonitoring.NewModule(telemetryStore, telemetryMetadataStore, querier, fl, providerSettings, config.InfraMonitoring),
|
||||
Promote: implpromote.NewModule(telemetryMetadataStore, telemetryStore),
|
||||
OrgGetter: orgGetter,
|
||||
OrgSetter: orgSetter,
|
||||
Preference: implpreference.NewModule(implpreference.NewStore(sqlstore), preferencetypes.NewAvailablePreference()),
|
||||
SavedView: implsavedview.NewModule(implsavedview.NewStore(sqlstore)),
|
||||
Apdex: implapdex.NewModule(sqlstore),
|
||||
Dashboard: dashboard,
|
||||
UserSetter: userSetter,
|
||||
UserGetter: userGetter,
|
||||
RetentionGetter: retentionGetter,
|
||||
QuickFilter: quickfilter,
|
||||
TraceFunnel: impltracefunnel.NewModule(impltracefunnel.NewStore(sqlstore)),
|
||||
RawDataExport: implrawdataexport.NewModule(querier),
|
||||
AuthDomain: authDomainModule,
|
||||
Session: implsession.NewModule(providerSettings, authNs, userSetter, userGetter, authDomainModule, tokenizer, orgGetter, authz, config.Global),
|
||||
SpanPercentile: implspanpercentile.NewModule(querier, providerSettings),
|
||||
Services: implservices.NewModule(querier, telemetryStore),
|
||||
MetricsExplorer: implmetricsexplorer.NewModule(telemetryStore, telemetryMetadataStore, cache, ruleStore, dashboard, fl, providerSettings, config.MetricsExplorer),
|
||||
MetricReductionRule: metricReductionRule,
|
||||
InfraMonitoring: implinframonitoring.NewModule(telemetryStore, telemetryMetadataStore, querier, fl, providerSettings, config.InfraMonitoring),
|
||||
Promote: implpromote.NewModule(telemetryMetadataStore, telemetryStore),
|
||||
ServiceAccount: serviceAccount,
|
||||
ServiceAccountGetter: serviceAccountGetter,
|
||||
LogsPipeline: impllogspipeline.NewModule(sqlstore),
|
||||
RuleStateHistory: implrulestatehistory.NewModule(implrulestatehistory.NewStore(telemetryStore, telemetryMetadataStore, providerSettings.Logger), ruleStore),
|
||||
CloudIntegration: cloudIntegrationModule,
|
||||
TraceDetail: impltracedetail.NewModule(impltracedetail.NewTraceStore(telemetryStore), providerSettings, config.TraceDetail),
|
||||
SpanMapper: implspanmapper.NewModule(implspanmapper.NewStore(sqlstore), fl),
|
||||
LLMPricingRule: impllmpricingrule.NewModule(impllmpricingrule.NewStore(sqlstore), fl, querier),
|
||||
Tag: tagModule,
|
||||
LogsPipeline: impllogspipeline.NewModule(sqlstore),
|
||||
RuleStateHistory: implrulestatehistory.NewModule(implrulestatehistory.NewStore(telemetryStore, telemetryMetadataStore, providerSettings.Logger), ruleStore),
|
||||
CloudIntegration: cloudIntegrationModule,
|
||||
TraceDetail: impltracedetail.NewModule(impltracedetail.NewTraceStore(telemetryStore), providerSettings, config.TraceDetail),
|
||||
SpanMapper: implspanmapper.NewModule(implspanmapper.NewStore(sqlstore), fl),
|
||||
LLMPricingRule: impllmpricingrule.NewModule(impllmpricingrule.NewStore(sqlstore), fl, querier),
|
||||
Tag: tagModule,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,9 +51,7 @@ func TestNewModules(t *testing.T) {
|
||||
queryParser := queryparser.New(providerSettings)
|
||||
require.NoError(t, err)
|
||||
tagModule := impltag.NewModule(impltag.NewStore(sqlstore))
|
||||
systemDashboardRegistry, err := impldashboard.NewSystemDashboardRegistry()
|
||||
require.NoError(t, err)
|
||||
dashboardModule := impldashboard.NewModule(impldashboard.NewStore(sqlstore), providerSettings, nil, orgGetter, queryParser, tagModule, systemDashboardRegistry)
|
||||
dashboardModule := impldashboard.NewModule(impldashboard.NewStore(sqlstore), providerSettings, nil, orgGetter, queryParser, tagModule)
|
||||
|
||||
flagger, err := flagger.New(context.Background(), instrumentationtest.New().ToProviderSettings(), flagger.Config{}, flagger.MustNewRegistry())
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -37,6 +37,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/modules/spanmapper"
|
||||
"github.com/SigNoz/signoz/pkg/modules/tracedetail"
|
||||
"github.com/SigNoz/signoz/pkg/modules/user"
|
||||
"github.com/SigNoz/signoz/pkg/prometheus"
|
||||
"github.com/SigNoz/signoz/pkg/querier"
|
||||
"github.com/SigNoz/signoz/pkg/ruler"
|
||||
"github.com/SigNoz/signoz/pkg/statsreporter"
|
||||
@@ -88,6 +89,7 @@ func NewOpenAPI(ctx context.Context, instrumentation instrumentation.Instrumenta
|
||||
struct{ rulestatehistory.Handler }{},
|
||||
struct{ spanmapper.Handler }{},
|
||||
struct{ alertmanager.Handler }{},
|
||||
struct{ prometheus.Handler }{},
|
||||
struct{ llmpricingrule.Handler }{},
|
||||
struct{ tracedetail.Handler }{},
|
||||
struct{ ruler.Handler }{},
|
||||
|
||||
@@ -245,7 +245,6 @@ func NewSQLMigrationProviderFactories(
|
||||
sqlmigration.NewMigrateLambdaDashboardsFactory(),
|
||||
sqlmigration.NewAddAuthDomainTuplesFactory(sqlstore),
|
||||
sqlmigration.NewAddDeploymentHostTuplesFactory(sqlstore),
|
||||
sqlmigration.NewAddSystemDashboardFactory(sqlstore, sqlschema),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -344,6 +343,7 @@ func NewAPIServerProviderFactories(orgGetter organization.Getter, authz authz.Au
|
||||
handlers.RuleStateHistory,
|
||||
handlers.SpanMapperHandler,
|
||||
handlers.AlertmanagerHandler,
|
||||
handlers.PrometheusHandler,
|
||||
handlers.LLMPricingRuleHandler,
|
||||
handlers.TraceDetail,
|
||||
handlers.RulerHandler,
|
||||
|
||||
@@ -60,7 +60,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/telemetrystore"
|
||||
pkgtokenizer "github.com/SigNoz/signoz/pkg/tokenizer"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/SigNoz/signoz/pkg/version"
|
||||
@@ -176,7 +175,7 @@ func New(
|
||||
telemetrystoreProviderFactories factory.NamedMap[factory.ProviderFactory[telemetrystore.TelemetryStore, telemetrystore.Config]],
|
||||
authNsCallback func(ctx context.Context, providerSettings factory.ProviderSettings, store authtypes.AuthNStore, licensing licensing.Licensing) (map[authtypes.AuthNProvider]authn.AuthN, error),
|
||||
authzCallback func(context.Context, sqlstore.SQLStore, authz.Config, licensing.Licensing, []authz.OnBeforeRoleDelete) (factory.ProviderFactory[authz.AuthZ, authz.Config], error),
|
||||
dashboardModuleCallback func(sqlstore.SQLStore, factory.ProviderSettings, analytics.Analytics, organization.Getter, queryparser.QueryParser, querier.Querier, licensing.Licensing, tag.Module, dashboardtypes.SystemDashboardRegistry) dashboard.Module,
|
||||
dashboardModuleCallback func(sqlstore.SQLStore, factory.ProviderSettings, analytics.Analytics, organization.Getter, queryparser.QueryParser, querier.Querier, licensing.Licensing, tag.Module) dashboard.Module,
|
||||
gatewayProviderFactory func(licensing.Licensing) factory.ProviderFactory[gateway.Gateway, gateway.Config],
|
||||
auditorProviderFactories func(licensing.Licensing) factory.NamedMap[factory.ProviderFactory[auditor.Auditor, auditor.Config]],
|
||||
meterReporterProviderFactories func(context.Context, factory.ProviderSettings, flagger.Flagger, licensing.Licensing, telemetrystore.TelemetryStore, retention.Getter, organization.Getter, zeus.Zeus) (factory.NamedMap[factory.ProviderFactory[meterreporter.Reporter, meterreporter.Config]], string),
|
||||
@@ -441,13 +440,8 @@ func New(
|
||||
// Initialize query parser (needed for dashboard module)
|
||||
queryParser := queryparser.New(providerSettings)
|
||||
|
||||
// Initialize dashboard module. The system dashboard registry is parsed here so
|
||||
// a malformed embedded definition fails startup instead of a request.
|
||||
systemDashboardRegistry, err := impldashboard.NewSystemDashboardRegistry()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dashboard := dashboardModuleCallback(sqlstore, providerSettings, analytics, orgGetter, queryParser, querier, licensing, tagModule, systemDashboardRegistry)
|
||||
// Initialize dashboard module
|
||||
dashboard := dashboardModuleCallback(sqlstore, providerSettings, analytics, orgGetter, queryParser, querier, licensing, tagModule)
|
||||
|
||||
// Initialize user getter
|
||||
userGetter := impluser.NewGetter(userStore, userRoleStore, flagger)
|
||||
@@ -616,7 +610,6 @@ func New(
|
||||
factory.NewNamedService(factory.MustNewName("auditor"), auditor),
|
||||
factory.NewNamedService(factory.MustNewName("meterreporter"), meterReporter, factory.MustNewName("licensing")),
|
||||
factory.NewNamedService(factory.MustNewName("ruler"), rulerInstance),
|
||||
factory.NewNamedService(factory.MustNewName("systemdashboard"), impldashboard.NewService(providerSettings, dashboard, orgGetter)),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -624,7 +617,7 @@ func New(
|
||||
|
||||
// Initialize all handlers for the modules
|
||||
registryHandler := factory.NewHandler(registry)
|
||||
handlers := NewHandlers(modules, providerSettings, analytics, querierHandler, licensing, global, flagger, gateway, telemetryMetadataStore, authz, zeus, registryHandler, alertmanager, rulerInstance, statsAggregator)
|
||||
handlers := NewHandlers(modules, providerSettings, analytics, querierHandler, licensing, global, flagger, gateway, telemetryMetadataStore, authz, zeus, registryHandler, alertmanager, prometheus, rulerInstance, statsAggregator)
|
||||
|
||||
// Initialize the API server (after registry so it can access service health)
|
||||
apiserverInstance, err := factory.NewProviderFromNamedMap(
|
||||
|
||||
@@ -1,93 +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 addSystemDashboard struct {
|
||||
sqlstore sqlstore.SQLStore
|
||||
sqlschema sqlschema.SQLSchema
|
||||
}
|
||||
|
||||
func NewAddSystemDashboardFactory(sqlstore sqlstore.SQLStore, sqlschema sqlschema.SQLSchema) factory.ProviderFactory[SQLMigration, Config] {
|
||||
return factory.NewProviderFactory(
|
||||
factory.MustNewName("add_system_dashboard"),
|
||||
func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
|
||||
return &addSystemDashboard{sqlstore: sqlstore, sqlschema: sqlschema}, nil
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func (migration *addSystemDashboard) Register(migrations *migrate.Migrations) error {
|
||||
return migrations.Register(migration.Up, migration.Down)
|
||||
}
|
||||
|
||||
func (migration *addSystemDashboard) 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: "system_dashboard",
|
||||
Columns: []*sqlschema.Column{
|
||||
{Name: "id", DataType: sqlschema.DataTypeText, Nullable: false},
|
||||
{Name: "org_id", DataType: sqlschema.DataTypeText, Nullable: false},
|
||||
{Name: "dashboard_id", DataType: sqlschema.DataTypeText, Nullable: false},
|
||||
{Name: "name", DataType: sqlschema.DataTypeText, Nullable: false},
|
||||
{Name: "version", DataType: sqlschema.DataTypeBigInt, 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"),
|
||||
},
|
||||
{
|
||||
ReferencingColumnName: sqlschema.ColumnName("dashboard_id"),
|
||||
ReferencedTableName: sqlschema.TableName("dashboard"),
|
||||
ReferencedColumnName: sqlschema.ColumnName("id"),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// (org_id, name) is what makes provisioning safe across replicas: the state
|
||||
// row is written in the same transaction as the dashboard, so a losing racer
|
||||
// rolls back its dashboard too.
|
||||
sqls = append(sqls, migration.sqlschema.Operator().CreateIndex(
|
||||
&sqlschema.UniqueIndex{
|
||||
TableName: "system_dashboard",
|
||||
ColumnNames: []sqlschema.ColumnName{"org_id", "name"},
|
||||
},
|
||||
)...)
|
||||
sqls = append(sqls, migration.sqlschema.Operator().CreateIndex(
|
||||
&sqlschema.UniqueIndex{
|
||||
TableName: "system_dashboard",
|
||||
ColumnNames: []sqlschema.ColumnName{"dashboard_id"},
|
||||
},
|
||||
)...)
|
||||
|
||||
for _, sql := range sqls {
|
||||
if _, err := tx.ExecContext(ctx, string(sql)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (migration *addSystemDashboard) Down(context.Context, *bun.DB) error {
|
||||
return nil
|
||||
}
|
||||
@@ -25,10 +25,6 @@ const (
|
||||
dashboardNameSuffixLen = 8
|
||||
)
|
||||
|
||||
// SystemDashboardNamePrefix is reserved for dashboards SigNoz ships and owns. Generated
|
||||
// names never contain consecutive hyphens, so only a typed name can carry it — create rejects that.
|
||||
const SystemDashboardNamePrefix = "signoz---"
|
||||
|
||||
const (
|
||||
dashboardIconPathPrefix = "/assets/Icons/"
|
||||
dashboardLogoPathPrefix = "/assets/Logos/"
|
||||
@@ -79,8 +75,8 @@ type DashboardV2 struct {
|
||||
}
|
||||
|
||||
func (d *DashboardV2) ErrIfNotMutable() error {
|
||||
if d.Source != SourceUser {
|
||||
return errors.Newf(errors.TypeInvalidInput, ErrCodeDashboardImmutable, "%s dashboards cannot be modified", d.Source)
|
||||
if d.Source == SourceIntegration {
|
||||
return errors.Newf(errors.TypeInvalidInput, ErrCodeDashboardImmutable, "integration dashboards cannot be modified")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -99,11 +95,6 @@ func (d *DashboardV2) Update(updatable UpdatableDashboardV2, updatedBy string, r
|
||||
if err := d.ErrIfNotUpdatable(); err != nil {
|
||||
return err
|
||||
}
|
||||
return d.UpdateUnsafe(updatable, updatedBy, resolvedTags)
|
||||
}
|
||||
|
||||
// UpdateUnsafe applies the update without the source/lock gate. Intended for internal system callers.
|
||||
func (d *DashboardV2) UpdateUnsafe(updatable UpdatableDashboardV2, updatedBy string, resolvedTags []*tagtypes.Tag) error {
|
||||
if updatable.Name != d.Name {
|
||||
return errors.NewInvalidInputf(ErrCodeDashboardImmutable, "name is immutable; cannot change from %q to %q", d.Name, updatable.Name)
|
||||
}
|
||||
@@ -138,13 +129,6 @@ func (d *DashboardV2) LockUnlock(lock bool, isAdmin bool, updatedBy string) erro
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DashboardV2) ErrIfNotSystem() error {
|
||||
if d.Source != SourceSystem {
|
||||
return errors.Newf(errors.TypeNotFound, ErrCodeDashboardNotFound, "dashboard %q is not a system dashboard", d.Name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DashboardV2) ErrIfNotClonable() error {
|
||||
if !d.Source.isClonable() {
|
||||
return errors.Newf(errors.TypeInvalidInput, ErrCodeDashboardImmutable, "%s dashboards cannot be cloned", d.Source)
|
||||
@@ -221,21 +205,13 @@ type PostableDashboardV2 struct {
|
||||
Spec DashboardSpec `json:"spec" required:"true"`
|
||||
}
|
||||
|
||||
func (postable PostableDashboardV2) NewDashboardV2(orgID valuer.UUID, createdBy string, source Source) (*DashboardV2, error) {
|
||||
func (postable PostableDashboardV2) NewDashboardV2(orgID valuer.UUID, createdBy string, source Source) *DashboardV2 {
|
||||
now := time.Now()
|
||||
|
||||
name := postable.Name
|
||||
if postable.GenerateName {
|
||||
name = generateDashboardName(postable.Spec.Display.Name)
|
||||
}
|
||||
// Checked on the final name, here rather than in validateName, because only
|
||||
// the constructor knows the source.
|
||||
if source != SourceSystem && strings.HasPrefix(name, SystemDashboardNamePrefix) {
|
||||
return nil, errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "name %q is invalid: the %q prefix is reserved for system dashboards", name, SystemDashboardNamePrefix)
|
||||
}
|
||||
if source == SourceSystem && !strings.HasPrefix(name, SystemDashboardNamePrefix) {
|
||||
return nil, errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "name %q is invalid: system dashboard names must start with the %q prefix", name, SystemDashboardNamePrefix)
|
||||
}
|
||||
|
||||
return &DashboardV2{
|
||||
Identifiable: types.Identifiable{ID: valuer.GenerateUUID()},
|
||||
@@ -248,7 +224,7 @@ func (postable PostableDashboardV2) NewDashboardV2(orgID valuer.UUID, createdBy
|
||||
Name: name,
|
||||
Tags: tagtypes.NewTagsFromPostableTags(orgID, coretypes.KindDashboard, postable.Tags),
|
||||
Spec: postable.Spec,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PostableDashboardV2) UnmarshalJSON(data []byte) error {
|
||||
@@ -389,36 +365,6 @@ func (d DashboardV2) ToGettableDashboardV2() GettableDashboardV2 {
|
||||
}
|
||||
}
|
||||
|
||||
// GettableSystemDashboard is the system-dashboard endpoint's response. System
|
||||
// dashboards are addressed by their stable definition name, so it carries no id.
|
||||
type GettableSystemDashboard struct {
|
||||
types.TimeAuditable
|
||||
types.UserAuditable
|
||||
|
||||
OrgID valuer.UUID `json:"orgId" required:"true"`
|
||||
Locked bool `json:"locked" required:"true"`
|
||||
Source Source `json:"source" required:"true"`
|
||||
|
||||
DashboardV2MetadataBase
|
||||
Name string `json:"name" required:"true"`
|
||||
Tags []*tagtypes.GettableTag `json:"tags" required:"true"`
|
||||
Spec DashboardSpec `json:"spec" required:"true"`
|
||||
}
|
||||
|
||||
func (d DashboardV2) ToGettableSystemDashboard() GettableSystemDashboard {
|
||||
return GettableSystemDashboard{
|
||||
TimeAuditable: d.TimeAuditable,
|
||||
UserAuditable: d.UserAuditable,
|
||||
OrgID: d.OrgID,
|
||||
Locked: d.Locked,
|
||||
Source: d.Source,
|
||||
DashboardV2MetadataBase: d.DashboardV2MetadataBase,
|
||||
Name: d.Name,
|
||||
Tags: tagtypes.NewGettableTagsFromTags(d.Tags),
|
||||
Spec: d.Spec,
|
||||
}
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
// Storable
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -89,25 +89,21 @@ func TestPostableDashboardV2NewDashboardV2(t *testing.T) {
|
||||
cases := []struct {
|
||||
scenario string
|
||||
source Source
|
||||
name string
|
||||
expectedLocked bool
|
||||
}{
|
||||
{
|
||||
scenario: "user source is not locked",
|
||||
source: SourceUser,
|
||||
name: "my-dashboard",
|
||||
expectedLocked: false,
|
||||
},
|
||||
{
|
||||
scenario: "system source is not locked",
|
||||
source: SourceSystem,
|
||||
name: SystemDashboardNamePrefix + "my-dashboard",
|
||||
expectedLocked: false,
|
||||
},
|
||||
{
|
||||
scenario: "integration source is locked",
|
||||
source: SourceIntegration,
|
||||
name: "my-dashboard",
|
||||
expectedLocked: true,
|
||||
},
|
||||
}
|
||||
@@ -119,7 +115,7 @@ func TestPostableDashboardV2NewDashboardV2(t *testing.T) {
|
||||
SchemaVersion: SchemaVersion,
|
||||
Image: "img",
|
||||
},
|
||||
Name: tc.name,
|
||||
Name: "my-dashboard",
|
||||
Tags: []tagtypes.PostableTag{
|
||||
{Key: "team", Value: "platform"},
|
||||
{Key: "env", Value: "prod"},
|
||||
@@ -128,8 +124,7 @@ func TestPostableDashboardV2NewDashboardV2(t *testing.T) {
|
||||
}
|
||||
|
||||
before := time.Now()
|
||||
dashboard, err := postable.NewDashboardV2(orgID, "alice", tc.source)
|
||||
require.NoError(t, err)
|
||||
dashboard := postable.NewDashboardV2(orgID, "alice", tc.source)
|
||||
after := time.Now()
|
||||
|
||||
require.NotNil(t, dashboard)
|
||||
@@ -165,10 +160,8 @@ func TestPostableDashboardV2NewDashboardV2(t *testing.T) {
|
||||
Spec: DashboardSpec{},
|
||||
}
|
||||
|
||||
first, err := postable.NewDashboardV2(orgID, "alice", SourceUser)
|
||||
require.NoError(t, err)
|
||||
second, err := postable.NewDashboardV2(orgID, "alice", SourceUser)
|
||||
require.NoError(t, err)
|
||||
first := postable.NewDashboardV2(orgID, "alice", SourceUser)
|
||||
second := postable.NewDashboardV2(orgID, "alice", SourceUser)
|
||||
assert.NotEqual(t, first.ID, second.ID, "expected distinct UUIDs across invocations")
|
||||
})
|
||||
|
||||
@@ -181,8 +174,7 @@ func TestPostableDashboardV2NewDashboardV2(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
dashboard, err := postable.NewDashboardV2(orgID, "alice", SourceUser)
|
||||
require.NoError(t, err)
|
||||
dashboard := postable.NewDashboardV2(orgID, "alice", SourceUser)
|
||||
assert.True(t, strings.HasPrefix(dashboard.Name, "my-dashboard-"), "expected slug prefix, got %q", dashboard.Name)
|
||||
assert.Len(t, dashboard.Name, len("my-dashboard-")+dashboardNameSuffixLen)
|
||||
})
|
||||
|
||||
@@ -109,8 +109,7 @@ func TestPatchableDashboardV2_Apply(t *testing.T) {
|
||||
var p PostableDashboardV2
|
||||
require.NoError(t, json.Unmarshal([]byte(basePostableJSON), &p), "base postable JSON must validate")
|
||||
testOrgID := valuer.GenerateUUID()
|
||||
base, err := p.NewDashboardV2(testOrgID, "somecreatedthisiguess@signoz.io", SourceUser)
|
||||
require.NoError(t, err)
|
||||
base := p.NewDashboardV2(testOrgID, "somecreatedthisiguess@signoz.io", SourceUser)
|
||||
base.Tags = []*tagtypes.Tag{
|
||||
{Key: "team", Value: "alpha"},
|
||||
{Key: "env", Value: "prod"},
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/perses/spec/go/dashboard"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -1929,37 +1928,3 @@ func TestEnsureSingleExpressionAggregation(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Guards the constant: a prefixed name must stay a valid DNS-1123 label.
|
||||
func TestSystemDashboardNamePrefix(t *testing.T) {
|
||||
require.NoError(t, validateDashboardName(SystemDashboardNamePrefix+"ai-o11y-overview"))
|
||||
}
|
||||
|
||||
func TestNewDashboardV2RejectsReservedName(t *testing.T) {
|
||||
testCases := []struct {
|
||||
description string
|
||||
name string
|
||||
source Source
|
||||
errContains string
|
||||
}{
|
||||
{description: "reserved name for a system dashboard", name: SystemDashboardNamePrefix + "overview", source: SourceSystem},
|
||||
{description: "reserved name for a user dashboard", name: SystemDashboardNamePrefix + "overview", source: SourceUser, errContains: "reserved for system dashboards"},
|
||||
{description: "reserved name for an integration dashboard", name: SystemDashboardNamePrefix + "overview", source: SourceIntegration, errContains: "reserved for system dashboards"},
|
||||
{description: "unprefixed name for a system dashboard", name: "overview", source: SourceSystem, errContains: "must start with"},
|
||||
{description: "ordinary name for a user dashboard", name: "overview", source: SourceUser},
|
||||
{description: "fewer hyphens than the prefix for a user dashboard", name: "signoz--overview", source: SourceUser},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.description, func(t *testing.T) {
|
||||
postable := PostableDashboardV2{Name: testCase.name}
|
||||
_, err := postable.NewDashboardV2(valuer.GenerateUUID(), "user@signoz.io", testCase.source)
|
||||
if testCase.errContains != "" {
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), testCase.errContains)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,9 +13,6 @@ type Store interface {
|
||||
|
||||
Get(context.Context, valuer.UUID, valuer.UUID) (*StorableDashboard, error)
|
||||
|
||||
// GetByName resolves a dashboard by its per-org unique name.
|
||||
GetByName(ctx context.Context, orgID valuer.UUID, name string) (*StorableDashboard, error)
|
||||
|
||||
GetPublic(context.Context, string) (*StorablePublicDashboard, error)
|
||||
|
||||
GetDashboardByOrgsAndPublicID(context.Context, []string, string) (*StorableDashboard, error)
|
||||
@@ -75,13 +72,4 @@ type Store interface {
|
||||
UpdateDashboardView(ctx context.Context, view *DashboardView) error
|
||||
|
||||
DeleteDashboardView(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
// System dashboard methods
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
CreateSystemDashboard(ctx context.Context, storable *StorableSystemDashboard) error
|
||||
|
||||
GetSystemDashboard(ctx context.Context, orgID valuer.UUID, name string) (*StorableSystemDashboard, error)
|
||||
|
||||
UpdateSystemDashboardVersion(ctx context.Context, orgID valuer.UUID, name string, version int) error
|
||||
}
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
package dashboardtypes
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrCodeSystemDashboardNotFound = errors.MustNewCode("system_dashboard_not_found")
|
||||
ErrCodeSystemDashboardDefinitionInvalid = errors.MustNewCode("system_dashboard_definition_invalid")
|
||||
ErrCodeSystemDashboardAlreadyProvisioned = errors.MustNewCode("system_dashboard_already_provisioned")
|
||||
)
|
||||
|
||||
// ProvisionerIdentity is stamped into created_by/updated_by by the reconciler.
|
||||
const ProvisionerIdentity = "signoz"
|
||||
|
||||
// StorableSystemDashboard records the shipped version each org's copy of a system
|
||||
// dashboard was last provisioned at. That version is the only thing the dashboard
|
||||
// row cannot answer, since the binary only embeds the latest definition.
|
||||
type StorableSystemDashboard struct {
|
||||
bun.BaseModel `bun:"table:system_dashboard"`
|
||||
|
||||
types.Identifiable
|
||||
types.TimeAuditable
|
||||
OrgID valuer.UUID `bun:"org_id,type:text,notnull"`
|
||||
DashboardID valuer.UUID `bun:"dashboard_id,type:text,notnull"`
|
||||
Name string `bun:"name,type:text,notnull"`
|
||||
Version int `bun:"version,notnull"`
|
||||
}
|
||||
|
||||
func NewStorableSystemDashboard(orgID valuer.UUID, dashboardID valuer.UUID, name string, version int) *StorableSystemDashboard {
|
||||
now := time.Now()
|
||||
return &StorableSystemDashboard{
|
||||
Identifiable: types.Identifiable{ID: valuer.GenerateUUID()},
|
||||
TimeAuditable: types.TimeAuditable{CreatedAt: now, UpdatedAt: now},
|
||||
OrgID: orgID,
|
||||
DashboardID: dashboardID,
|
||||
Name: name,
|
||||
Version: version,
|
||||
}
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
package dashboardtypes
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
)
|
||||
|
||||
// SystemDashboardDefinition is one shipped system dashboard. Version is bumped on
|
||||
// every content change and drives upgrade detection; the name is the stable key
|
||||
// and never changes.
|
||||
type SystemDashboardDefinition struct {
|
||||
Version int `json:"version"`
|
||||
Dashboard PostableDashboardV2 `json:"definition"`
|
||||
}
|
||||
|
||||
func (definition SystemDashboardDefinition) Name() string {
|
||||
return definition.Dashboard.Name
|
||||
}
|
||||
|
||||
func NewSystemDashboardDefinition(raw []byte) (SystemDashboardDefinition, error) {
|
||||
decoder := json.NewDecoder(bytes.NewReader(raw))
|
||||
decoder.DisallowUnknownFields()
|
||||
|
||||
var definition SystemDashboardDefinition
|
||||
if err := decoder.Decode(&definition); err != nil {
|
||||
return SystemDashboardDefinition{}, errors.WrapInvalidInputf(err, ErrCodeSystemDashboardDefinitionInvalid, "%s", err.Error())
|
||||
}
|
||||
if err := definition.validate(); err != nil {
|
||||
return SystemDashboardDefinition{}, err
|
||||
}
|
||||
|
||||
return definition, nil
|
||||
}
|
||||
|
||||
func (definition SystemDashboardDefinition) validate() error {
|
||||
if definition.Version < 1 {
|
||||
return errors.NewInvalidInputf(ErrCodeSystemDashboardDefinitionInvalid, "version must be at least 1, got %d", definition.Version)
|
||||
}
|
||||
if !strings.HasPrefix(definition.Name(), SystemDashboardNamePrefix) {
|
||||
return errors.NewInvalidInputf(ErrCodeSystemDashboardDefinitionInvalid, "name %q must start with %q", definition.Name(), SystemDashboardNamePrefix)
|
||||
}
|
||||
if definition.Dashboard.GenerateName {
|
||||
return errors.NewInvalidInputf(ErrCodeSystemDashboardDefinitionInvalid, "%s: generateName is not allowed, the name is the stable key", definition.Name())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToUpdatable is how an upgrade re-applies a definition onto an existing row:
|
||||
// everything but the dashboard's identity comes from the shipped definition.
|
||||
func (definition SystemDashboardDefinition) ToUpdatable() UpdatableDashboardV2 {
|
||||
return UpdatableDashboardV2{
|
||||
DashboardV2MetadataBase: definition.Dashboard.DashboardV2MetadataBase,
|
||||
Name: definition.Dashboard.Name,
|
||||
Tags: definition.Dashboard.Tags,
|
||||
Spec: definition.Dashboard.Spec,
|
||||
}
|
||||
}
|
||||
|
||||
// SystemDashboardRegistry holds every definition embedded in the binary, keyed by name.
|
||||
type SystemDashboardRegistry struct {
|
||||
definitions map[string]SystemDashboardDefinition
|
||||
}
|
||||
|
||||
func NewSystemDashboardRegistry(definitions []SystemDashboardDefinition) (SystemDashboardRegistry, error) {
|
||||
byName := make(map[string]SystemDashboardDefinition, len(definitions))
|
||||
for _, definition := range definitions {
|
||||
if _, duplicate := byName[definition.Name()]; duplicate {
|
||||
return SystemDashboardRegistry{}, errors.NewInvalidInputf(ErrCodeSystemDashboardDefinitionInvalid, "duplicate system dashboard name %q", definition.Name())
|
||||
}
|
||||
byName[definition.Name()] = definition
|
||||
}
|
||||
|
||||
return SystemDashboardRegistry{definitions: byName}, nil
|
||||
}
|
||||
|
||||
func (registry SystemDashboardRegistry) Get(name string) (SystemDashboardDefinition, bool) {
|
||||
definition, ok := registry.definitions[name]
|
||||
return definition, ok
|
||||
}
|
||||
|
||||
// List returns the definitions sorted by name so provisioning order is stable.
|
||||
func (registry SystemDashboardRegistry) List() []SystemDashboardDefinition {
|
||||
definitions := make([]SystemDashboardDefinition, 0, len(registry.definitions))
|
||||
for _, definition := range registry.definitions {
|
||||
definitions = append(definitions, definition)
|
||||
}
|
||||
slices.SortFunc(definitions, func(a, b SystemDashboardDefinition) int { return strings.Compare(a.Name(), b.Name()) })
|
||||
|
||||
return definitions
|
||||
}
|
||||
66
tests/fixtures/promqltestcorpus.py
vendored
Normal file
66
tests/fixtures/promqltestcorpus.py
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
from collections.abc import Callable
|
||||
from datetime import UTC, datetime, timedelta
|
||||
|
||||
from fixtures.metrics import Metrics
|
||||
|
||||
TESTDATA_DIR = os.path.join(os.path.dirname(__file__), "..", "integration", "testdata", "promqltestcorpus")
|
||||
CORPUS_FILE = os.path.join(TESTDATA_DIR, "corpus.json")
|
||||
|
||||
# Datasets sit on disjoint time windows (2h gaps, far beyond the 5m lookback)
|
||||
# so one bulk ingest serves every case without cross-talk.
|
||||
ISOLATION_GAP_MS = 2 * 3600 * 1000
|
||||
SPECIALS = {"NaN": math.nan, "Inf": math.inf, "-Inf": -math.inf}
|
||||
|
||||
|
||||
def ingest_promqltest_corpus(insert_metrics: Callable[[list[Metrics]], None]) -> tuple[dict, dict[int, int]]:
|
||||
"""Loads the frozen corpus, lays its datasets end to end on the timeline
|
||||
(newest last, ending safely in the past), ingests every sample, and
|
||||
returns (corpus, dataset base timestamps).
|
||||
|
||||
Dataset bases are hour-aligned: registration rows are hour-bucketed, so
|
||||
behavior depends on where samples fall relative to hour boundaries, and
|
||||
exact known-divergences enforcement needs identical placement every run."""
|
||||
with open(CORPUS_FILE, encoding="utf-8") as f:
|
||||
corpus = json.load(f)
|
||||
|
||||
cases_by_dataset: dict[int, list[dict]] = {}
|
||||
for case in corpus["cases"]:
|
||||
cases_by_dataset.setdefault(case["dataset"], []).append(case)
|
||||
|
||||
spans = {}
|
||||
for ds in corpus["datasets"]:
|
||||
sample_max = max((s["samples"][-1][0] for s in ds["series"] if s["samples"]), default=0)
|
||||
case_max = max((c["end_ms"] for c in cases_by_dataset.get(ds["id"], [])), default=0)
|
||||
spans[ds["id"]] = max(sample_max, case_max) + corpus["meta"]["lookback_ms"]
|
||||
|
||||
hour_ms = 3_600_000
|
||||
advances = {ds["id"]: -(-(spans[ds["id"]] + ISOLATION_GAP_MS) // hour_ms) * hour_ms for ds in corpus["datasets"]}
|
||||
total = sum(advances.values())
|
||||
now = datetime.now(tz=UTC).replace(second=0, microsecond=0)
|
||||
cursor = (int((now - timedelta(hours=1)).timestamp() * 1000) - total) // hour_ms * hour_ms
|
||||
|
||||
bases: dict[int, int] = {}
|
||||
metrics: list[Metrics] = []
|
||||
for ds in corpus["datasets"]:
|
||||
bases[ds["id"]] = cursor
|
||||
for series in ds["series"]:
|
||||
labels = dict(series["labels"])
|
||||
metric_name = labels.pop("__name__")
|
||||
for off_ms, raw in series["samples"]:
|
||||
stale = raw == "stale"
|
||||
metrics.append(
|
||||
Metrics(
|
||||
metric_name=metric_name,
|
||||
labels=labels,
|
||||
timestamp=datetime.fromtimestamp((cursor + off_ms) / 1000, tz=UTC),
|
||||
value=0.0 if stale else (SPECIALS[raw] if isinstance(raw, str) else float(raw)),
|
||||
flags=1 if stale else 0,
|
||||
)
|
||||
)
|
||||
cursor += advances[ds["id"]]
|
||||
|
||||
insert_metrics(metrics)
|
||||
return corpus, bases
|
||||
@@ -1,192 +0,0 @@
|
||||
from collections.abc import Callable
|
||||
from http import HTTPStatus
|
||||
|
||||
import requests
|
||||
from sqlalchemy import sql
|
||||
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
|
||||
from fixtures.dashboards import DASHBOARDS_BASE_URL, MAX_LIST_LIMIT
|
||||
from fixtures.types import Operation, SigNoz
|
||||
|
||||
SYSTEM_BASE_URL = "/api/v2/dashboards/system"
|
||||
|
||||
# Provisioned for every org by the reconciler; the path segment is the bare
|
||||
# definition name, the stored name carries the reserved prefix.
|
||||
SYSTEM_DASHBOARD_NAME = "ai-o11y-overview"
|
||||
SYSTEM_DASHBOARD_PREFIX = "signoz---"
|
||||
|
||||
|
||||
def test_get_system_dashboard(
|
||||
signoz: SigNoz,
|
||||
create_user_admin: Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
):
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get(f"{SYSTEM_BASE_URL}/{SYSTEM_DASHBOARD_NAME}"),
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=5,
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
dashboard = response.json()["data"]
|
||||
assert dashboard["name"] == SYSTEM_DASHBOARD_PREFIX + SYSTEM_DASHBOARD_NAME
|
||||
assert dashboard["source"] == "system"
|
||||
assert dashboard["createdBy"] == "signoz"
|
||||
assert dashboard["schemaVersion"] == "v6"
|
||||
assert "id" not in dashboard
|
||||
|
||||
|
||||
def test_get_system_dashboard_rejects_prefixed_name(
|
||||
signoz: SigNoz,
|
||||
create_user_admin: Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
):
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get(f"{SYSTEM_BASE_URL}/{SYSTEM_DASHBOARD_PREFIX}{SYSTEM_DASHBOARD_NAME}"),
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=5,
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.BAD_REQUEST, response.text
|
||||
assert "must not carry" in response.json()["error"]["message"]
|
||||
|
||||
|
||||
def test_get_missing_system_dashboard_returns_not_found(
|
||||
signoz: SigNoz,
|
||||
create_user_admin: Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
):
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get(f"{SYSTEM_BASE_URL}/no-such-dashboard"),
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=5,
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.NOT_FOUND, response.text
|
||||
|
||||
|
||||
def test_system_dashboard_hidden_from_list_but_gettable_by_id(
|
||||
signoz: SigNoz,
|
||||
create_user_admin: Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
):
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
# The API never exposes a system dashboard's id; read it from the state row.
|
||||
with signoz.sqlstore.conn.connect() as conn:
|
||||
dashboard_id = conn.execute(
|
||||
sql.text("SELECT dashboard_id FROM system_dashboard WHERE name = :name"),
|
||||
{"name": SYSTEM_DASHBOARD_PREFIX + SYSTEM_DASHBOARD_NAME},
|
||||
).scalar_one()
|
||||
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get(f"{DASHBOARDS_BASE_URL}?limit={MAX_LIST_LIMIT}"),
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
listed = response.json()["data"]["dashboards"] or []
|
||||
assert all(dashboard["source"] != "system" for dashboard in listed)
|
||||
assert all(dashboard["id"] != dashboard_id for dashboard in listed)
|
||||
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get(f"{DASHBOARDS_BASE_URL}/{dashboard_id}"),
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
assert response.json()["data"]["source"] == "system"
|
||||
|
||||
|
||||
def test_system_dashboard_is_immutable(
|
||||
signoz: SigNoz,
|
||||
create_user_admin: Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
):
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get(f"{SYSTEM_BASE_URL}/{SYSTEM_DASHBOARD_NAME}"),
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
dashboard = response.json()["data"]
|
||||
|
||||
with signoz.sqlstore.conn.connect() as conn:
|
||||
dashboard_id = conn.execute(
|
||||
sql.text("SELECT dashboard_id FROM system_dashboard WHERE name = :name"),
|
||||
{"name": SYSTEM_DASHBOARD_PREFIX + SYSTEM_DASHBOARD_NAME},
|
||||
).scalar_one()
|
||||
|
||||
response = requests.put(
|
||||
signoz.self.host_configs["8080"].get(f"{DASHBOARDS_BASE_URL}/{dashboard_id}"),
|
||||
json={
|
||||
"schemaVersion": dashboard["schemaVersion"],
|
||||
"name": dashboard["name"],
|
||||
"tags": [],
|
||||
"spec": dashboard["spec"],
|
||||
},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.BAD_REQUEST, response.text
|
||||
assert response.json()["error"]["code"] == "dashboard_immutable"
|
||||
|
||||
response = requests.delete(
|
||||
signoz.self.host_configs["8080"].get(f"{DASHBOARDS_BASE_URL}/{dashboard_id}"),
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.BAD_REQUEST, response.text
|
||||
assert response.json()["error"]["code"] == "dashboard_immutable"
|
||||
|
||||
response = requests.put(
|
||||
signoz.self.host_configs["8080"].get(f"{DASHBOARDS_BASE_URL}/{dashboard_id}/lock"),
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.BAD_REQUEST, response.text
|
||||
assert response.json()["error"]["code"] == "dashboard_immutable"
|
||||
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get(f"{DASHBOARDS_BASE_URL}/{dashboard_id}/clone"),
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.BAD_REQUEST, response.text
|
||||
assert response.json()["error"]["code"] == "dashboard_immutable"
|
||||
|
||||
|
||||
def test_create_rejects_reserved_prefix_name(
|
||||
signoz: SigNoz,
|
||||
create_user_admin: Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
):
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get(DASHBOARDS_BASE_URL),
|
||||
json={
|
||||
"schemaVersion": "v6",
|
||||
"name": f"{SYSTEM_DASHBOARD_PREFIX}custom",
|
||||
"tags": [],
|
||||
"spec": {
|
||||
"display": {"name": "Custom"},
|
||||
"variables": [],
|
||||
"panels": {},
|
||||
"layouts": [],
|
||||
},
|
||||
},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=5,
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.BAD_REQUEST, response.text
|
||||
assert "reserved for system dashboards" in response.json()["error"]["message"]
|
||||
@@ -0,0 +1,138 @@
|
||||
import json
|
||||
import math
|
||||
from collections.abc import Callable
|
||||
from http import HTTPStatus
|
||||
|
||||
import requests
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
|
||||
from fixtures.metrics import Metrics
|
||||
from fixtures.promqltestcorpus import ingest_promqltest_corpus
|
||||
|
||||
# The same frozen corpus the promqlconformance package replays through
|
||||
# /api/v5/query_range, here replayed against the /prometheus/api/v1 endpoints
|
||||
# with clickhousev2 as the serving provider (see conftest.py) — the two paths
|
||||
# nothing else exercises. Range cases go to query_range, where a
|
||||
# RangeExecutor provider serves transpiled statements when the shape allows.
|
||||
# Instant cases go to /query with a real `time` parameter, so they need no
|
||||
# grid encoding.
|
||||
#
|
||||
# Prometheus API sample values are strings, "NaN"/"+Inf"/"-Inf" included.
|
||||
SPECIALS = {"NaN": math.nan, "Inf": math.inf, "+Inf": math.inf, "-Inf": -math.inf}
|
||||
QUERY_TIMEOUT = 30
|
||||
|
||||
|
||||
def test_prometheus_api_corpus(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_metrics: Callable[[list[Metrics]], None],
|
||||
) -> None:
|
||||
corpus, bases = ingest_promqltest_corpus(insert_metrics)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
failures: list[str] = []
|
||||
for case in corpus["cases"]:
|
||||
# instant-coarse variants encode an instant eval as a coarse-step
|
||||
# range because the v5 API cannot run true instants. This API can:
|
||||
# the [base] form of the same eval goes through /query below, and the
|
||||
# transpiled coarse-step serving the encoding exercises is covered
|
||||
# (and its known divergences ledgered) by promqlconformance's
|
||||
# clickhousev2 leg.
|
||||
if case["variant"] == "instant-coarse":
|
||||
continue
|
||||
|
||||
base = bases[case["dataset"]]
|
||||
start_ms = base + case["start_ms"]
|
||||
end_ms = base + case["end_ms"]
|
||||
step_s = max(1, case["step_ms"] // 1000)
|
||||
case_id = f"{case['source']}[{case['variant']}]"
|
||||
|
||||
if case["instant"]:
|
||||
path, params = "/prometheus/api/v1/query", {"query": case["expr"], "time": end_ms / 1000}
|
||||
else:
|
||||
path, params = (
|
||||
"/prometheus/api/v1/query_range",
|
||||
{
|
||||
"query": case["expr"],
|
||||
"start": start_ms / 1000,
|
||||
"end": end_ms / 1000,
|
||||
"step": step_s,
|
||||
},
|
||||
)
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get(path),
|
||||
params=params,
|
||||
timeout=QUERY_TIMEOUT,
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
)
|
||||
if response.status_code != HTTPStatus.OK:
|
||||
failures.append(f"{case_id}: HTTP {response.status_code} for {case['expr']!r}: {response.text[:200]}")
|
||||
continue
|
||||
body = response.json()
|
||||
if body.get("status") != "success":
|
||||
failures.append(f"{case_id}: status {body.get('status')!r} for {case['expr']!r}: {json.dumps(body)[:200]}")
|
||||
continue
|
||||
|
||||
result_type, result = body["data"]["resultType"], body["data"]["result"]
|
||||
actual: dict[tuple, dict[int, float]] = {}
|
||||
if result_type == "matrix":
|
||||
for series in result:
|
||||
points = {round(float(ts) * 1000): SPECIALS[v] if v in SPECIALS else float(v) for ts, v in series.get("values") or []}
|
||||
actual[tuple(sorted((series.get("metric") or {}).items()))] = points
|
||||
elif result_type == "vector":
|
||||
for series in result:
|
||||
ts, v = series["value"]
|
||||
actual[tuple(sorted((series.get("metric") or {}).items()))] = {round(float(ts) * 1000): SPECIALS[v] if v in SPECIALS else float(v)}
|
||||
elif result_type == "scalar":
|
||||
ts, v = result
|
||||
actual[()] = {round(float(ts) * 1000): SPECIALS[v] if v in SPECIALS else float(v)}
|
||||
|
||||
expected: dict[tuple, dict[int, float]] = {}
|
||||
for res in case["expected"]:
|
||||
points = {base + off_ms: SPECIALS[v] if isinstance(v, str) else float(v) for off_ms, v in res["points"]}
|
||||
expected[tuple(sorted(res["labels"].items()))] = points
|
||||
|
||||
if set(actual) != set(expected):
|
||||
missing = set(expected) - set(actual)
|
||||
extra = set(actual) - set(expected)
|
||||
failures.append(f"{case_id}: series mismatch for {case['expr']!r} (missing={sorted(missing)[:3]} extra={sorted(extra)[:3]})")
|
||||
continue
|
||||
|
||||
mismatch = None
|
||||
for lset, exp_points in expected.items():
|
||||
act_points = actual[lset]
|
||||
if set(act_points) != set(exp_points):
|
||||
mismatch = f"{case_id}: timestamp mismatch for {case['expr']!r} series {dict(lset)} (expected {len(exp_points)} points, got {len(act_points)})"
|
||||
break
|
||||
for ts, exp_v in exp_points.items():
|
||||
act_v = act_points[ts]
|
||||
if math.isnan(act_v) or math.isnan(exp_v):
|
||||
close = math.isnan(act_v) and math.isnan(exp_v)
|
||||
elif math.isinf(act_v) or math.isinf(exp_v):
|
||||
close = act_v == exp_v
|
||||
elif act_v == exp_v:
|
||||
close = True
|
||||
else:
|
||||
# Expected values carry the v5 API's rounding (>=1: three
|
||||
# decimal places; <1: three significant digits); this API
|
||||
# returns raw floats. One rounding quantum covers the
|
||||
# largest possible rounding difference.
|
||||
scale = max(abs(act_v), abs(exp_v))
|
||||
if scale >= 1:
|
||||
quantum = max(1e-3, scale * 1e-9)
|
||||
else:
|
||||
quantum = 10 ** (math.floor(math.log10(scale)) - 2)
|
||||
close = abs(act_v - exp_v) <= quantum + 1e-12
|
||||
if not close:
|
||||
mismatch = f"{case_id}: value mismatch for {case['expr']!r} series {dict(lset)} at {ts}: expected {exp_v}, got {act_v}"
|
||||
break
|
||||
if mismatch:
|
||||
break
|
||||
if mismatch:
|
||||
failures.append(mismatch)
|
||||
|
||||
for f_line in failures:
|
||||
print("DIVERGED", f_line)
|
||||
assert not failures, f"{len(failures)} corpus cases diverged:\n" + "\n".join(failures[:25])
|
||||
37
tests/integration/tests/promapiconformance/conftest.py
Normal file
37
tests/integration/tests/promapiconformance/conftest.py
Normal file
@@ -0,0 +1,37 @@
|
||||
import pytest
|
||||
from testcontainers.core.container import Network
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.signoz import create_signoz
|
||||
|
||||
|
||||
@pytest.fixture(name="signoz", scope="package")
|
||||
def signoz_promapi_v2(
|
||||
network: Network,
|
||||
migrator: types.Operation, # pylint: disable=unused-argument
|
||||
zeus: types.TestContainerDocker,
|
||||
gateway: types.TestContainerDocker,
|
||||
sqlstore: types.TestContainerSQL,
|
||||
clickhouse: types.TestContainerClickhouse,
|
||||
request: pytest.FixtureRequest,
|
||||
pytestconfig: pytest.Config,
|
||||
) -> types.SigNoz:
|
||||
"""
|
||||
SigNoz with clickhousev2 as the serving prometheus provider. The corpus
|
||||
replays against the /prometheus/api/v1 endpoints, so this package covers
|
||||
the two paths nothing else serves: v2 as the provider (range queries
|
||||
transpile when the shape allows), and the Prometheus HTTP API contract.
|
||||
"""
|
||||
return create_signoz(
|
||||
network=network,
|
||||
zeus=zeus,
|
||||
gateway=gateway,
|
||||
sqlstore=sqlstore,
|
||||
clickhouse=clickhouse,
|
||||
request=request,
|
||||
pytestconfig=pytestconfig,
|
||||
cache_key="signoz-promapi-v2",
|
||||
env_overrides={
|
||||
"SIGNOZ_PROMETHEUS_PROVIDER": "clickhousev2",
|
||||
},
|
||||
)
|
||||
@@ -2,21 +2,21 @@ import json
|
||||
import math
|
||||
import os
|
||||
from collections.abc import Callable
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from http import HTTPStatus
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
|
||||
from fixtures.metrics import Metrics
|
||||
from fixtures.promqltestcorpus import ingest_promqltest_corpus
|
||||
from fixtures.querier import get_all_series, make_query_request
|
||||
|
||||
TESTDATA_DIR = os.path.join(os.path.dirname(__file__), "..", "..", "testdata")
|
||||
# Frozen corpus extracted from Prometheus' own promql/promqltest testdata by
|
||||
# scripts/promqltestcorpus (upstream load scripts + the vendored reference engine).
|
||||
# Unlike live-vs-live parity suites, the oracle is this committed file, so the suite
|
||||
# keeps working when the serving path itself is the thing being changed — the one
|
||||
# situation where comparing two live paths against each other is blind.
|
||||
CORPUS_FILE = os.path.join(TESTDATA_DIR, "promqltestcorpus", "corpus.json")
|
||||
# The corpus (see fixtures/promqltestcorpus.py) is frozen from Prometheus' own
|
||||
# promql/promqltest testdata by scripts/promqltestcorpus (upstream load scripts
|
||||
# + the vendored reference engine). Unlike live-vs-live parity suites, the
|
||||
# oracle is a committed file, so the suite keeps working when the serving path
|
||||
# itself is the thing being changed — the one situation where comparing two
|
||||
# live paths against each other is blind.
|
||||
|
||||
# One ledger per leg, enforced exactly in both directions. The default leg's
|
||||
# ledger is empty and pinned there; the clickhousev2 ledger is the rollout
|
||||
@@ -40,9 +40,6 @@ LEGS: list[tuple[str, dict | None]] = [
|
||||
("clickhousev2", {"X-SigNoz-PromQL-Provider": "clickhousev2"}),
|
||||
]
|
||||
|
||||
# Datasets sit on disjoint time windows (2h gaps, far beyond the 5m lookback) so
|
||||
# one bulk ingest serves every case without cross-talk.
|
||||
ISOLATION_GAP_MS = 2 * 3600 * 1000
|
||||
SPECIALS = {"NaN": math.nan, "Inf": math.inf, "-Inf": -math.inf}
|
||||
|
||||
|
||||
@@ -52,51 +49,7 @@ def test_upstream_promqltest_corpus(
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_metrics: Callable[[list[Metrics]], None],
|
||||
) -> None:
|
||||
with open(CORPUS_FILE, encoding="utf-8") as f:
|
||||
corpus = json.load(f)
|
||||
|
||||
cases_by_dataset: dict[int, list[dict]] = {}
|
||||
for case in corpus["cases"]:
|
||||
cases_by_dataset.setdefault(case["dataset"], []).append(case)
|
||||
|
||||
# Lay datasets end to end on the timeline, newest last, ending safely in
|
||||
# the past; spans are per-dataset so the whole corpus stays within days.
|
||||
spans = {}
|
||||
for ds in corpus["datasets"]:
|
||||
sample_max = max((s["samples"][-1][0] for s in ds["series"] if s["samples"]), default=0)
|
||||
case_max = max((c["end_ms"] for c in cases_by_dataset.get(ds["id"], [])), default=0)
|
||||
spans[ds["id"]] = max(sample_max, case_max) + corpus["meta"]["lookback_ms"]
|
||||
|
||||
# Hour-aligned dataset bases: registration rows are hour-bucketed, so
|
||||
# behavior depends on where samples fall relative to hour boundaries —
|
||||
# the exact known-divergences enforcement needs that identical every run.
|
||||
hour_ms = 3_600_000
|
||||
advances = {ds["id"]: -(-(spans[ds["id"]] + ISOLATION_GAP_MS) // hour_ms) * hour_ms for ds in corpus["datasets"]}
|
||||
total = sum(advances.values())
|
||||
now = datetime.now(tz=UTC).replace(second=0, microsecond=0)
|
||||
cursor = (int((now - timedelta(hours=1)).timestamp() * 1000) - total) // hour_ms * hour_ms
|
||||
|
||||
bases: dict[int, int] = {}
|
||||
metrics: list[Metrics] = []
|
||||
for ds in corpus["datasets"]:
|
||||
bases[ds["id"]] = cursor
|
||||
for series in ds["series"]:
|
||||
labels = dict(series["labels"])
|
||||
metric_name = labels.pop("__name__")
|
||||
for off_ms, raw in series["samples"]:
|
||||
stale = raw == "stale"
|
||||
metrics.append(
|
||||
Metrics(
|
||||
metric_name=metric_name,
|
||||
labels=labels,
|
||||
timestamp=datetime.fromtimestamp((cursor + off_ms) / 1000, tz=UTC),
|
||||
value=0.0 if stale else (SPECIALS[raw] if isinstance(raw, str) else float(raw)),
|
||||
flags=1 if stale else 0,
|
||||
)
|
||||
)
|
||||
cursor += advances[ds["id"]]
|
||||
|
||||
insert_metrics(metrics)
|
||||
corpus, bases = ingest_promqltest_corpus(insert_metrics)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
failures: dict[str, list[str]] = {leg: [] for leg, _ in LEGS}
|
||||
|
||||
Reference in New Issue
Block a user