mirror of
https://github.com/SigNoz/signoz.git
synced 2026-04-21 11:20:28 +01:00
Compare commits
1 Commits
feat/json-
...
nv/dashboa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bbaf64c4f0 |
3
.github/workflows/integrationci.yaml
vendored
3
.github/workflows/integrationci.yaml
vendored
@@ -52,7 +52,6 @@ jobs:
|
||||
- ingestionkeys
|
||||
- rootuser
|
||||
- serviceaccount
|
||||
- querier_json_body
|
||||
sqlstore-provider:
|
||||
- postgres
|
||||
- sqlite
|
||||
@@ -62,7 +61,7 @@ jobs:
|
||||
- 25.5.6
|
||||
- 25.12.5
|
||||
schema-migrator-version:
|
||||
- v0.144.3
|
||||
- v0.142.0
|
||||
postgres-version:
|
||||
- 15
|
||||
if: |
|
||||
|
||||
@@ -1035,6 +1035,18 @@ components:
|
||||
required:
|
||||
- config
|
||||
type: object
|
||||
CommonDisplay:
|
||||
properties:
|
||||
description:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
type: object
|
||||
CommonJSONRef:
|
||||
properties:
|
||||
$ref:
|
||||
type: string
|
||||
type: object
|
||||
ConfigAuthorization:
|
||||
properties:
|
||||
credentials:
|
||||
@@ -1986,6 +1998,43 @@ components:
|
||||
to_user:
|
||||
type: string
|
||||
type: object
|
||||
DashboardGridItem:
|
||||
properties:
|
||||
content:
|
||||
$ref: '#/components/schemas/CommonJSONRef'
|
||||
height:
|
||||
type: integer
|
||||
width:
|
||||
type: integer
|
||||
x:
|
||||
type: integer
|
||||
"y":
|
||||
type: integer
|
||||
type: object
|
||||
DashboardGridLayoutCollapse:
|
||||
properties:
|
||||
open:
|
||||
type: boolean
|
||||
type: object
|
||||
DashboardGridLayoutDisplay:
|
||||
properties:
|
||||
collapse:
|
||||
$ref: '#/components/schemas/DashboardGridLayoutCollapse'
|
||||
title:
|
||||
type: string
|
||||
type: object
|
||||
DashboardGridLayoutSpec:
|
||||
properties:
|
||||
display:
|
||||
$ref: '#/components/schemas/DashboardGridLayoutDisplay'
|
||||
items:
|
||||
items:
|
||||
$ref: '#/components/schemas/DashboardGridItem'
|
||||
nullable: true
|
||||
type: array
|
||||
repeatVariable:
|
||||
type: string
|
||||
type: object
|
||||
DashboardtypesDashboard:
|
||||
properties:
|
||||
createdAt:
|
||||
@@ -2040,6 +2089,749 @@ components:
|
||||
timeRangeEnabled:
|
||||
type: boolean
|
||||
type: object
|
||||
Dashboardtypesv2Axes:
|
||||
properties:
|
||||
isLogScale:
|
||||
type: boolean
|
||||
softMax:
|
||||
nullable: true
|
||||
type: number
|
||||
softMin:
|
||||
nullable: true
|
||||
type: number
|
||||
type: object
|
||||
Dashboardtypesv2BarChartPanelSpec:
|
||||
properties:
|
||||
axes:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2Axes'
|
||||
formatting:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2PanelFormatting'
|
||||
legend:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2Legend'
|
||||
thresholds:
|
||||
items:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2ThresholdWithLabel'
|
||||
nullable: true
|
||||
type: array
|
||||
visualization:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2BarChartVisualization'
|
||||
type: object
|
||||
Dashboardtypesv2BarChartVisualization:
|
||||
properties:
|
||||
fillSpans:
|
||||
type: boolean
|
||||
stackedBarChart:
|
||||
type: boolean
|
||||
timePreference:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2TimePreference'
|
||||
type: object
|
||||
Dashboardtypesv2BasicVisualization:
|
||||
properties:
|
||||
timePreference:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2TimePreference'
|
||||
type: object
|
||||
Dashboardtypesv2BuilderQuerySpec:
|
||||
oneOf:
|
||||
- $ref: '#/components/schemas/Querybuildertypesv5QueryBuilderQueryGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5LogAggregation'
|
||||
- $ref: '#/components/schemas/Querybuildertypesv5QueryBuilderQueryGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5MetricAggregation'
|
||||
- $ref: '#/components/schemas/Querybuildertypesv5QueryBuilderQueryGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5TraceAggregation'
|
||||
Dashboardtypesv2ComparisonOperator:
|
||||
enum:
|
||||
- '>'
|
||||
- <
|
||||
- '>='
|
||||
- <=
|
||||
- =
|
||||
- above
|
||||
- below
|
||||
- above_or_equal
|
||||
- below_or_equal
|
||||
- equal
|
||||
- not_equal
|
||||
type: string
|
||||
Dashboardtypesv2ComparisonThreshold:
|
||||
properties:
|
||||
color:
|
||||
type: string
|
||||
format:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2ThresholdFormat'
|
||||
operator:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2ComparisonOperator'
|
||||
unit:
|
||||
type: string
|
||||
value:
|
||||
format: double
|
||||
type: number
|
||||
required:
|
||||
- value
|
||||
- color
|
||||
type: object
|
||||
Dashboardtypesv2CustomVariableSpec:
|
||||
properties:
|
||||
customValue:
|
||||
type: string
|
||||
required:
|
||||
- customValue
|
||||
type: object
|
||||
Dashboardtypesv2DashboardData:
|
||||
properties:
|
||||
datasources:
|
||||
additionalProperties:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2DatasourceSpec'
|
||||
type: object
|
||||
display:
|
||||
$ref: '#/components/schemas/CommonDisplay'
|
||||
duration:
|
||||
type: string
|
||||
layouts:
|
||||
items:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2Layout'
|
||||
nullable: true
|
||||
type: array
|
||||
links:
|
||||
items:
|
||||
$ref: '#/components/schemas/V1Link'
|
||||
type: array
|
||||
panels:
|
||||
additionalProperties:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2Panel'
|
||||
nullable: true
|
||||
type: object
|
||||
refreshInterval:
|
||||
type: string
|
||||
variables:
|
||||
items:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2Variable'
|
||||
type: array
|
||||
type: object
|
||||
Dashboardtypesv2DatasourcePlugin:
|
||||
oneOf:
|
||||
- $ref: '#/components/schemas/Dashboardtypesv2DatasourcePluginVariantStruct'
|
||||
Dashboardtypesv2DatasourcePluginKind:
|
||||
enum:
|
||||
- signoz/Datasource
|
||||
type: string
|
||||
Dashboardtypesv2DatasourcePluginVariantStruct:
|
||||
properties:
|
||||
kind:
|
||||
enum:
|
||||
- signoz/Datasource
|
||||
type: string
|
||||
spec:
|
||||
nullable: true
|
||||
type: object
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
type: object
|
||||
Dashboardtypesv2DatasourceSpec:
|
||||
properties:
|
||||
default:
|
||||
type: boolean
|
||||
display:
|
||||
$ref: '#/components/schemas/CommonDisplay'
|
||||
plugin:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2DatasourcePlugin'
|
||||
type: object
|
||||
Dashboardtypesv2DynamicVariableSpec:
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
signal:
|
||||
$ref: '#/components/schemas/TelemetrytypesSignal'
|
||||
required:
|
||||
- name
|
||||
type: object
|
||||
Dashboardtypesv2FillMode:
|
||||
enum:
|
||||
- solid
|
||||
- gradient
|
||||
- none
|
||||
type: string
|
||||
Dashboardtypesv2HistogramBuckets:
|
||||
properties:
|
||||
bucketCount:
|
||||
nullable: true
|
||||
type: number
|
||||
bucketWidth:
|
||||
nullable: true
|
||||
type: number
|
||||
mergeAllActiveQueries:
|
||||
type: boolean
|
||||
type: object
|
||||
Dashboardtypesv2HistogramPanelSpec:
|
||||
properties:
|
||||
histogramBuckets:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2HistogramBuckets'
|
||||
legend:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2Legend'
|
||||
type: object
|
||||
Dashboardtypesv2Layout:
|
||||
oneOf:
|
||||
- $ref: '#/components/schemas/Dashboardtypesv2LayoutEnvelopeGithubComPersesPersesPkgModelApiV1DashboardGridLayoutSpec'
|
||||
Dashboardtypesv2LayoutEnvelopeGithubComPersesPersesPkgModelApiV1DashboardGridLayoutSpec:
|
||||
properties:
|
||||
kind:
|
||||
enum:
|
||||
- Grid
|
||||
type: string
|
||||
spec:
|
||||
$ref: '#/components/schemas/DashboardGridLayoutSpec'
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
type: object
|
||||
Dashboardtypesv2Legend:
|
||||
properties:
|
||||
customColors:
|
||||
additionalProperties:
|
||||
type: string
|
||||
nullable: true
|
||||
type: object
|
||||
position:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2LegendPosition'
|
||||
type: object
|
||||
Dashboardtypesv2LegendPosition:
|
||||
enum:
|
||||
- bottom
|
||||
- right
|
||||
type: string
|
||||
Dashboardtypesv2LineInterpolation:
|
||||
enum:
|
||||
- linear
|
||||
- spline
|
||||
- step_after
|
||||
- step_before
|
||||
type: string
|
||||
Dashboardtypesv2LineStyle:
|
||||
enum:
|
||||
- solid
|
||||
- dashed
|
||||
type: string
|
||||
Dashboardtypesv2ListPanelSpec:
|
||||
properties:
|
||||
selectFields:
|
||||
items:
|
||||
$ref: '#/components/schemas/TelemetrytypesTelemetryFieldKey'
|
||||
type: array
|
||||
type: object
|
||||
Dashboardtypesv2ListVariableSpec:
|
||||
properties:
|
||||
allowAllValue:
|
||||
type: boolean
|
||||
allowMultiple:
|
||||
type: boolean
|
||||
capturingRegexp:
|
||||
type: string
|
||||
customAllValue:
|
||||
type: string
|
||||
defaultValue:
|
||||
$ref: '#/components/schemas/VariableDefaultValue'
|
||||
display:
|
||||
$ref: '#/components/schemas/VariableDisplay'
|
||||
name:
|
||||
type: string
|
||||
plugin:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2VariablePlugin'
|
||||
sort:
|
||||
nullable: true
|
||||
type: string
|
||||
type: object
|
||||
Dashboardtypesv2NumberPanelSpec:
|
||||
properties:
|
||||
formatting:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2PanelFormatting'
|
||||
thresholds:
|
||||
items:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2ComparisonThreshold'
|
||||
nullable: true
|
||||
type: array
|
||||
visualization:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2BasicVisualization'
|
||||
type: object
|
||||
Dashboardtypesv2Panel:
|
||||
properties:
|
||||
kind:
|
||||
type: string
|
||||
spec:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2PanelSpec'
|
||||
type: object
|
||||
Dashboardtypesv2PanelFormatting:
|
||||
properties:
|
||||
decimalPrecision:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2PrecisionOption'
|
||||
unit:
|
||||
type: string
|
||||
type: object
|
||||
Dashboardtypesv2PanelPlugin:
|
||||
oneOf:
|
||||
- $ref: '#/components/schemas/Dashboardtypesv2PanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesDashboardtypesv2TimeSeriesPanelSpec'
|
||||
- $ref: '#/components/schemas/Dashboardtypesv2PanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesDashboardtypesv2BarChartPanelSpec'
|
||||
- $ref: '#/components/schemas/Dashboardtypesv2PanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesDashboardtypesv2NumberPanelSpec'
|
||||
- $ref: '#/components/schemas/Dashboardtypesv2PanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesDashboardtypesv2PieChartPanelSpec'
|
||||
- $ref: '#/components/schemas/Dashboardtypesv2PanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesDashboardtypesv2TablePanelSpec'
|
||||
- $ref: '#/components/schemas/Dashboardtypesv2PanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesDashboardtypesv2HistogramPanelSpec'
|
||||
- $ref: '#/components/schemas/Dashboardtypesv2PanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesDashboardtypesv2ListPanelSpec'
|
||||
Dashboardtypesv2PanelPluginKind:
|
||||
enum:
|
||||
- signoz/TimeSeriesPanel
|
||||
- signoz/BarChartPanel
|
||||
- signoz/NumberPanel
|
||||
- signoz/PieChartPanel
|
||||
- signoz/TablePanel
|
||||
- signoz/HistogramPanel
|
||||
- signoz/ListPanel
|
||||
type: string
|
||||
Dashboardtypesv2PanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesDashboardtypesv2BarChartPanelSpec:
|
||||
properties:
|
||||
kind:
|
||||
enum:
|
||||
- signoz/BarChartPanel
|
||||
type: string
|
||||
spec:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2BarChartPanelSpec'
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
type: object
|
||||
Dashboardtypesv2PanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesDashboardtypesv2HistogramPanelSpec:
|
||||
properties:
|
||||
kind:
|
||||
enum:
|
||||
- signoz/HistogramPanel
|
||||
type: string
|
||||
spec:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2HistogramPanelSpec'
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
type: object
|
||||
Dashboardtypesv2PanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesDashboardtypesv2ListPanelSpec:
|
||||
properties:
|
||||
kind:
|
||||
enum:
|
||||
- signoz/ListPanel
|
||||
type: string
|
||||
spec:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2ListPanelSpec'
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
type: object
|
||||
Dashboardtypesv2PanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesDashboardtypesv2NumberPanelSpec:
|
||||
properties:
|
||||
kind:
|
||||
enum:
|
||||
- signoz/NumberPanel
|
||||
type: string
|
||||
spec:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2NumberPanelSpec'
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
type: object
|
||||
Dashboardtypesv2PanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesDashboardtypesv2PieChartPanelSpec:
|
||||
properties:
|
||||
kind:
|
||||
enum:
|
||||
- signoz/PieChartPanel
|
||||
type: string
|
||||
spec:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2PieChartPanelSpec'
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
type: object
|
||||
Dashboardtypesv2PanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesDashboardtypesv2TablePanelSpec:
|
||||
properties:
|
||||
kind:
|
||||
enum:
|
||||
- signoz/TablePanel
|
||||
type: string
|
||||
spec:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2TablePanelSpec'
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
type: object
|
||||
Dashboardtypesv2PanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesDashboardtypesv2TimeSeriesPanelSpec:
|
||||
properties:
|
||||
kind:
|
||||
enum:
|
||||
- signoz/TimeSeriesPanel
|
||||
type: string
|
||||
spec:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2TimeSeriesPanelSpec'
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
type: object
|
||||
Dashboardtypesv2PanelSpec:
|
||||
properties:
|
||||
display:
|
||||
$ref: '#/components/schemas/V1PanelDisplay'
|
||||
links:
|
||||
items:
|
||||
$ref: '#/components/schemas/V1Link'
|
||||
type: array
|
||||
plugin:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2PanelPlugin'
|
||||
queries:
|
||||
items:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2Query'
|
||||
type: array
|
||||
type: object
|
||||
Dashboardtypesv2PieChartPanelSpec:
|
||||
properties:
|
||||
formatting:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2PanelFormatting'
|
||||
legend:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2Legend'
|
||||
visualization:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2BasicVisualization'
|
||||
type: object
|
||||
Dashboardtypesv2PrecisionOption:
|
||||
enum:
|
||||
- "0"
|
||||
- "1"
|
||||
- "2"
|
||||
- "3"
|
||||
- "4"
|
||||
- full
|
||||
type: string
|
||||
Dashboardtypesv2Query:
|
||||
properties:
|
||||
kind:
|
||||
type: string
|
||||
spec:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2QuerySpec'
|
||||
type: object
|
||||
Dashboardtypesv2QueryPlugin:
|
||||
oneOf:
|
||||
- $ref: '#/components/schemas/Dashboardtypesv2QueryPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesDashboardtypesv2BuilderQuerySpec'
|
||||
- $ref: '#/components/schemas/Dashboardtypesv2QueryPluginVariantGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5CompositeQuery'
|
||||
- $ref: '#/components/schemas/Dashboardtypesv2QueryPluginVariantGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5QueryBuilderFormula'
|
||||
- $ref: '#/components/schemas/Dashboardtypesv2QueryPluginVariantGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5PromQuery'
|
||||
- $ref: '#/components/schemas/Dashboardtypesv2QueryPluginVariantGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5ClickHouseQuery'
|
||||
- $ref: '#/components/schemas/Dashboardtypesv2QueryPluginVariantGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5QueryBuilderTraceOperator'
|
||||
Dashboardtypesv2QueryPluginKind:
|
||||
enum:
|
||||
- signoz/BuilderQuery
|
||||
- signoz/CompositeQuery
|
||||
- signoz/Formula
|
||||
- signoz/PromQLQuery
|
||||
- signoz/ClickHouseSQL
|
||||
- signoz/TraceOperator
|
||||
type: string
|
||||
Dashboardtypesv2QueryPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesDashboardtypesv2BuilderQuerySpec:
|
||||
properties:
|
||||
kind:
|
||||
enum:
|
||||
- signoz/BuilderQuery
|
||||
type: string
|
||||
spec:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2BuilderQuerySpec'
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
type: object
|
||||
Dashboardtypesv2QueryPluginVariantGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5ClickHouseQuery:
|
||||
properties:
|
||||
kind:
|
||||
enum:
|
||||
- signoz/ClickHouseSQL
|
||||
type: string
|
||||
spec:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5ClickHouseQuery'
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
type: object
|
||||
Dashboardtypesv2QueryPluginVariantGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5CompositeQuery:
|
||||
properties:
|
||||
kind:
|
||||
enum:
|
||||
- signoz/CompositeQuery
|
||||
type: string
|
||||
spec:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5CompositeQuery'
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
type: object
|
||||
Dashboardtypesv2QueryPluginVariantGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5PromQuery:
|
||||
properties:
|
||||
kind:
|
||||
enum:
|
||||
- signoz/PromQLQuery
|
||||
type: string
|
||||
spec:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5PromQuery'
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
type: object
|
||||
Dashboardtypesv2QueryPluginVariantGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5QueryBuilderFormula:
|
||||
properties:
|
||||
kind:
|
||||
enum:
|
||||
- signoz/Formula
|
||||
type: string
|
||||
spec:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5QueryBuilderFormula'
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
type: object
|
||||
Dashboardtypesv2QueryPluginVariantGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5QueryBuilderTraceOperator:
|
||||
properties:
|
||||
kind:
|
||||
enum:
|
||||
- signoz/TraceOperator
|
||||
type: string
|
||||
spec:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5QueryBuilderTraceOperator'
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
type: object
|
||||
Dashboardtypesv2QuerySpec:
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
plugin:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2QueryPlugin'
|
||||
type: object
|
||||
Dashboardtypesv2QueryVariableSpec:
|
||||
properties:
|
||||
queryValue:
|
||||
type: string
|
||||
required:
|
||||
- queryValue
|
||||
type: object
|
||||
Dashboardtypesv2SpanGaps:
|
||||
properties:
|
||||
fillLessThan:
|
||||
type: string
|
||||
fillOnlyBelow:
|
||||
type: boolean
|
||||
type: object
|
||||
Dashboardtypesv2TableFormatting:
|
||||
properties:
|
||||
columnUnits:
|
||||
additionalProperties:
|
||||
type: string
|
||||
nullable: true
|
||||
type: object
|
||||
decimalPrecision:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2PrecisionOption'
|
||||
type: object
|
||||
Dashboardtypesv2TablePanelSpec:
|
||||
properties:
|
||||
formatting:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2TableFormatting'
|
||||
thresholds:
|
||||
items:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2TableThreshold'
|
||||
nullable: true
|
||||
type: array
|
||||
visualization:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2BasicVisualization'
|
||||
type: object
|
||||
Dashboardtypesv2TableThreshold:
|
||||
properties:
|
||||
color:
|
||||
type: string
|
||||
columnName:
|
||||
type: string
|
||||
format:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2ThresholdFormat'
|
||||
operator:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2ComparisonOperator'
|
||||
unit:
|
||||
type: string
|
||||
value:
|
||||
format: double
|
||||
type: number
|
||||
required:
|
||||
- value
|
||||
- color
|
||||
- columnName
|
||||
type: object
|
||||
Dashboardtypesv2TextVariableSpec:
|
||||
properties:
|
||||
constant:
|
||||
type: boolean
|
||||
display:
|
||||
$ref: '#/components/schemas/VariableDisplay'
|
||||
name:
|
||||
type: string
|
||||
value:
|
||||
type: string
|
||||
type: object
|
||||
Dashboardtypesv2TextboxVariableSpec:
|
||||
type: object
|
||||
Dashboardtypesv2ThresholdFormat:
|
||||
enum:
|
||||
- text
|
||||
- background
|
||||
type: string
|
||||
Dashboardtypesv2ThresholdWithLabel:
|
||||
properties:
|
||||
color:
|
||||
type: string
|
||||
label:
|
||||
type: string
|
||||
unit:
|
||||
type: string
|
||||
value:
|
||||
format: double
|
||||
type: number
|
||||
required:
|
||||
- value
|
||||
- color
|
||||
- label
|
||||
type: object
|
||||
Dashboardtypesv2TimePreference:
|
||||
enum:
|
||||
- global_time
|
||||
- last_5_min
|
||||
- last_15_min
|
||||
- last_30_min
|
||||
- last_1_hr
|
||||
- last_6_hr
|
||||
- last_1_day
|
||||
- last_3_days
|
||||
- last_1_week
|
||||
- last_1_month
|
||||
type: string
|
||||
Dashboardtypesv2TimeSeriesChartAppearance:
|
||||
properties:
|
||||
fillMode:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2FillMode'
|
||||
lineInterpolation:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2LineInterpolation'
|
||||
lineStyle:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2LineStyle'
|
||||
showPoints:
|
||||
type: boolean
|
||||
spanGaps:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2SpanGaps'
|
||||
type: object
|
||||
Dashboardtypesv2TimeSeriesPanelSpec:
|
||||
properties:
|
||||
axes:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2Axes'
|
||||
chartAppearance:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2TimeSeriesChartAppearance'
|
||||
formatting:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2PanelFormatting'
|
||||
legend:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2Legend'
|
||||
thresholds:
|
||||
items:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2ThresholdWithLabel'
|
||||
nullable: true
|
||||
type: array
|
||||
visualization:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2TimeSeriesVisualization'
|
||||
type: object
|
||||
Dashboardtypesv2TimeSeriesVisualization:
|
||||
properties:
|
||||
fillSpans:
|
||||
type: boolean
|
||||
timePreference:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2TimePreference'
|
||||
type: object
|
||||
Dashboardtypesv2Variable:
|
||||
oneOf:
|
||||
- $ref: '#/components/schemas/Dashboardtypesv2VariableEnvelopeGithubComSigNozSignozPkgTypesDashboardtypesDashboardtypesv2ListVariableSpec'
|
||||
- $ref: '#/components/schemas/Dashboardtypesv2VariableEnvelopeGithubComSigNozSignozPkgTypesDashboardtypesDashboardtypesv2TextVariableSpec'
|
||||
Dashboardtypesv2VariableEnvelopeGithubComSigNozSignozPkgTypesDashboardtypesDashboardtypesv2ListVariableSpec:
|
||||
properties:
|
||||
kind:
|
||||
enum:
|
||||
- ListVariable
|
||||
type: string
|
||||
spec:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2ListVariableSpec'
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
type: object
|
||||
Dashboardtypesv2VariableEnvelopeGithubComSigNozSignozPkgTypesDashboardtypesDashboardtypesv2TextVariableSpec:
|
||||
properties:
|
||||
kind:
|
||||
enum:
|
||||
- TextVariable
|
||||
type: string
|
||||
spec:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2TextVariableSpec'
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
type: object
|
||||
Dashboardtypesv2VariablePlugin:
|
||||
oneOf:
|
||||
- $ref: '#/components/schemas/Dashboardtypesv2VariablePluginVariantGithubComSigNozSignozPkgTypesDashboardtypesDashboardtypesv2DynamicVariableSpec'
|
||||
- $ref: '#/components/schemas/Dashboardtypesv2VariablePluginVariantGithubComSigNozSignozPkgTypesDashboardtypesDashboardtypesv2QueryVariableSpec'
|
||||
- $ref: '#/components/schemas/Dashboardtypesv2VariablePluginVariantGithubComSigNozSignozPkgTypesDashboardtypesDashboardtypesv2CustomVariableSpec'
|
||||
- $ref: '#/components/schemas/Dashboardtypesv2VariablePluginVariantGithubComSigNozSignozPkgTypesDashboardtypesDashboardtypesv2TextboxVariableSpec'
|
||||
Dashboardtypesv2VariablePluginKind:
|
||||
enum:
|
||||
- signoz/DynamicVariable
|
||||
- signoz/QueryVariable
|
||||
- signoz/CustomVariable
|
||||
- signoz/TextboxVariable
|
||||
type: string
|
||||
Dashboardtypesv2VariablePluginVariantGithubComSigNozSignozPkgTypesDashboardtypesDashboardtypesv2CustomVariableSpec:
|
||||
properties:
|
||||
kind:
|
||||
enum:
|
||||
- signoz/CustomVariable
|
||||
type: string
|
||||
spec:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2CustomVariableSpec'
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
type: object
|
||||
Dashboardtypesv2VariablePluginVariantGithubComSigNozSignozPkgTypesDashboardtypesDashboardtypesv2DynamicVariableSpec:
|
||||
properties:
|
||||
kind:
|
||||
enum:
|
||||
- signoz/DynamicVariable
|
||||
type: string
|
||||
spec:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2DynamicVariableSpec'
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
type: object
|
||||
Dashboardtypesv2VariablePluginVariantGithubComSigNozSignozPkgTypesDashboardtypesDashboardtypesv2QueryVariableSpec:
|
||||
properties:
|
||||
kind:
|
||||
enum:
|
||||
- signoz/QueryVariable
|
||||
type: string
|
||||
spec:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2QueryVariableSpec'
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
type: object
|
||||
Dashboardtypesv2VariablePluginVariantGithubComSigNozSignozPkgTypesDashboardtypesDashboardtypesv2TextboxVariableSpec:
|
||||
properties:
|
||||
kind:
|
||||
enum:
|
||||
- signoz/TextboxVariable
|
||||
type: string
|
||||
spec:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2TextboxVariableSpec'
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
type: object
|
||||
ErrorsJSON:
|
||||
properties:
|
||||
code:
|
||||
@@ -4551,6 +5343,37 @@ components:
|
||||
required:
|
||||
- id
|
||||
type: object
|
||||
V1Link:
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
renderVariables:
|
||||
type: boolean
|
||||
targetBlank:
|
||||
type: boolean
|
||||
tooltip:
|
||||
type: string
|
||||
url:
|
||||
type: string
|
||||
type: object
|
||||
V1PanelDisplay:
|
||||
properties:
|
||||
description:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
type: object
|
||||
VariableDefaultValue:
|
||||
type: object
|
||||
VariableDisplay:
|
||||
properties:
|
||||
description:
|
||||
type: string
|
||||
hidden:
|
||||
type: boolean
|
||||
name:
|
||||
type: string
|
||||
type: object
|
||||
ZeustypesGettableHost:
|
||||
properties:
|
||||
hosts:
|
||||
@@ -9345,6 +10168,58 @@ paths:
|
||||
summary: Update user preference
|
||||
tags:
|
||||
- preferences
|
||||
/api/v2/dashboards:
|
||||
post:
|
||||
deprecated: false
|
||||
description: 'TEMP: dummy endpoint to exercise v2 spec shape while we iterate
|
||||
on plugin schema.'
|
||||
operationId: CreateDashboardV2
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2DashboardData'
|
||||
responses:
|
||||
"201":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2DashboardData'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: Created
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- ADMIN
|
||||
- tokenizer:
|
||||
- ADMIN
|
||||
summary: Create dashboard v2
|
||||
tags:
|
||||
- dashboard
|
||||
/api/v2/factor_password/forgot:
|
||||
post:
|
||||
deprecated: false
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useMemo, useRef } from 'react';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import ChartLayout from 'container/DashboardContainer/visualization/layout/ChartLayout/ChartLayout';
|
||||
import Legend from 'lib/uPlotV2/components/Legend/Legend';
|
||||
import {
|
||||
@@ -30,7 +30,6 @@ export default function ChartWrapper({
|
||||
onDestroy = noop,
|
||||
children,
|
||||
layoutChildren,
|
||||
yAxisUnit,
|
||||
customTooltip,
|
||||
pinnedTooltipElement,
|
||||
'data-testid': testId,
|
||||
@@ -63,13 +62,6 @@ export default function ChartWrapper({
|
||||
[customTooltip],
|
||||
);
|
||||
|
||||
const syncMetadata = useMemo(
|
||||
() => ({
|
||||
yAxisUnit,
|
||||
}),
|
||||
[yAxisUnit],
|
||||
);
|
||||
|
||||
return (
|
||||
<PlotContextProvider>
|
||||
<ChartLayout
|
||||
@@ -107,7 +99,6 @@ export default function ChartWrapper({
|
||||
averageLegendWidth + TOOLTIP_WIDTH_PADDING,
|
||||
)}
|
||||
syncKey={syncKey}
|
||||
syncMetadata={syncMetadata}
|
||||
render={renderTooltipCallback}
|
||||
pinnedTooltipElement={pinnedTooltipElement}
|
||||
/>
|
||||
|
||||
@@ -24,12 +24,13 @@ export default function Histogram(props: HistogramChartProps): JSX.Element {
|
||||
}
|
||||
const tooltipProps: HistogramTooltipProps = {
|
||||
...props,
|
||||
timezone: rest.timezone,
|
||||
yAxisUnit: rest.yAxisUnit,
|
||||
decimalPrecision: rest.decimalPrecision,
|
||||
};
|
||||
return <HistogramTooltip {...tooltipProps} />;
|
||||
},
|
||||
[customTooltip, rest.yAxisUnit, rest.decimalPrecision],
|
||||
[customTooltip, rest.timezone, rest.yAxisUnit, rest.decimalPrecision],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -12,7 +12,10 @@ interface BaseChartProps {
|
||||
height: number;
|
||||
showTooltip?: boolean;
|
||||
showLegend?: boolean;
|
||||
timezone?: Timezone;
|
||||
canPinTooltip?: boolean;
|
||||
yAxisUnit?: string;
|
||||
decimalPrecision?: PrecisionOption;
|
||||
pinnedTooltipElement?: (clickData: TooltipClickData) => React.ReactNode;
|
||||
customTooltip?: (props: TooltipRenderArgs) => React.ReactNode;
|
||||
'data-testid'?: string;
|
||||
@@ -29,31 +32,18 @@ interface UPlotBasedChartProps {
|
||||
layoutChildren?: React.ReactNode;
|
||||
}
|
||||
|
||||
interface UPlotChartDataProps {
|
||||
yAxisUnit?: string;
|
||||
decimalPrecision?: PrecisionOption;
|
||||
}
|
||||
|
||||
export interface TimeSeriesChartProps
|
||||
extends BaseChartProps,
|
||||
UPlotBasedChartProps,
|
||||
UPlotChartDataProps {
|
||||
timezone?: Timezone;
|
||||
}
|
||||
UPlotBasedChartProps {}
|
||||
|
||||
export interface HistogramChartProps
|
||||
extends BaseChartProps,
|
||||
UPlotBasedChartProps,
|
||||
UPlotChartDataProps {
|
||||
UPlotBasedChartProps {
|
||||
isQueriesMerged?: boolean;
|
||||
}
|
||||
|
||||
export interface BarChartProps
|
||||
extends BaseChartProps,
|
||||
UPlotBasedChartProps,
|
||||
UPlotChartDataProps {
|
||||
export interface BarChartProps extends BaseChartProps, UPlotBasedChartProps {
|
||||
isStackedBarChart?: boolean;
|
||||
timezone?: Timezone;
|
||||
}
|
||||
|
||||
export type ChartProps =
|
||||
|
||||
@@ -123,13 +123,13 @@ function BarPanel(props: PanelWrapperProps): JSX.Element {
|
||||
}}
|
||||
plotRef={onPlotRef}
|
||||
onDestroy={onPlotDestroy}
|
||||
yAxisUnit={widget.yAxisUnit}
|
||||
decimalPrecision={widget.decimalPrecision}
|
||||
data={chartData as uPlot.AlignedData}
|
||||
width={containerDimensions.width}
|
||||
height={containerDimensions.height}
|
||||
layoutChildren={layoutChildren}
|
||||
isStackedBarChart={widget.stackedBarChart ?? false}
|
||||
yAxisUnit={widget.yAxisUnit}
|
||||
decimalPrecision={widget.decimalPrecision}
|
||||
timezone={timezone}
|
||||
>
|
||||
<ContextMenu
|
||||
|
||||
@@ -3,6 +3,8 @@ import { PanelWrapperProps } from 'container/PanelWrapper/panelWrapper.types';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useResizeObserver } from 'hooks/useDimensions';
|
||||
import { LegendPosition } from 'lib/uPlotV2/components/types';
|
||||
import { DashboardCursorSync } from 'lib/uPlotV2/plugins/TooltipPlugin/types';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import uPlot from 'uplot';
|
||||
|
||||
import Histogram from '../../charts/Histogram/Histogram';
|
||||
@@ -27,6 +29,7 @@ function HistogramPanel(props: PanelWrapperProps): JSX.Element {
|
||||
const containerDimensions = useResizeObserver(graphRef);
|
||||
|
||||
const isDarkMode = useIsDarkMode();
|
||||
const { timezone } = useTimezone();
|
||||
|
||||
const config = useMemo(() => {
|
||||
return prepareHistogramPanelConfig({
|
||||
@@ -89,9 +92,11 @@ function HistogramPanel(props: PanelWrapperProps): JSX.Element {
|
||||
onDestroy={(): void => {
|
||||
uPlotRef.current = null;
|
||||
}}
|
||||
isQueriesMerged={widget.mergeAllActiveQueries}
|
||||
yAxisUnit={widget.yAxisUnit}
|
||||
decimalPrecision={widget.decimalPrecision}
|
||||
isQueriesMerged={widget.mergeAllActiveQueries}
|
||||
syncMode={DashboardCursorSync.Crosshair}
|
||||
timezone={timezone}
|
||||
data={chartData as uPlot.AlignedData}
|
||||
width={containerDimensions.width}
|
||||
height={containerDimensions.height}
|
||||
|
||||
@@ -48,8 +48,8 @@ jest.mock(
|
||||
{JSON.stringify({
|
||||
legendPosition: props.legendConfig?.position,
|
||||
isQueriesMerged: props.isQueriesMerged,
|
||||
yAxisUnit: props?.yAxisUnit,
|
||||
decimalPrecision: props?.decimalPrecision,
|
||||
yAxisUnit: props.yAxisUnit,
|
||||
decimalPrecision: props.decimalPrecision,
|
||||
})}
|
||||
</div>
|
||||
{props.layoutChildren}
|
||||
|
||||
@@ -112,9 +112,9 @@ function TimeSeriesPanel(props: PanelWrapperProps): JSX.Element {
|
||||
legendConfig={{
|
||||
position: widget?.legendPosition ?? LegendPosition.BOTTOM,
|
||||
}}
|
||||
timezone={timezone}
|
||||
yAxisUnit={widget.yAxisUnit}
|
||||
decimalPrecision={widget.decimalPrecision}
|
||||
timezone={timezone}
|
||||
data={chartData as uPlot.AlignedData}
|
||||
width={containerDimensions.width}
|
||||
height={containerDimensions.height}
|
||||
|
||||
@@ -62,10 +62,10 @@ export interface TooltipRenderArgs {
|
||||
|
||||
export interface BaseTooltipProps {
|
||||
showTooltipHeader?: boolean;
|
||||
timezone?: Timezone;
|
||||
yAxisUnit?: string;
|
||||
decimalPrecision?: PrecisionOption;
|
||||
content?: TooltipContentItem[];
|
||||
timezone?: Timezone;
|
||||
}
|
||||
|
||||
export interface TimeSeriesTooltipProps
|
||||
|
||||
@@ -4,7 +4,6 @@ import cx from 'classnames';
|
||||
import { getFocusedSeriesAtPosition } from 'lib/uPlotLib/plugins/onClickPlugin';
|
||||
import uPlot from 'uplot';
|
||||
|
||||
import { syncCursorRegistry } from './syncCursorRegistry';
|
||||
import {
|
||||
createInitialControllerState,
|
||||
createSetCursorHandler,
|
||||
@@ -41,7 +40,6 @@ export default function TooltipPlugin({
|
||||
maxHeight = 600,
|
||||
syncMode = DashboardCursorSync.None,
|
||||
syncKey = '_tooltip_sync_global_',
|
||||
syncMetadata,
|
||||
pinnedTooltipElement,
|
||||
canPinTooltip = false,
|
||||
}: TooltipPluginProps): JSX.Element | null {
|
||||
@@ -102,29 +100,7 @@ export default function TooltipPlugin({
|
||||
// crosshair / tooltip can follow the dashboard-wide cursor.
|
||||
if (syncMode !== DashboardCursorSync.None && config.scales[0]?.props.time) {
|
||||
config.setCursor({
|
||||
sync: { key: syncKey, scales: ['x', 'y'] },
|
||||
});
|
||||
|
||||
// Show the horizontal crosshair only when the receiving panel shares
|
||||
// the same y-axis unit as the source panel. When this panel is the
|
||||
// source (cursor.event != null) the line is always shown and this
|
||||
// panel's metadata is written to the registry so receivers can read it.
|
||||
config.addHook('setCursor', (u: uPlot): void => {
|
||||
const yCursorEl = u.root.querySelector<HTMLElement>('.u-cursor-y');
|
||||
if (!yCursorEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (u.cursor.event != null) {
|
||||
// This panel is the source — publish metadata and always show line.
|
||||
syncCursorRegistry.setMetadata(syncKey, syncMetadata);
|
||||
yCursorEl.style.display = '';
|
||||
} else {
|
||||
// This panel is receiving sync — show only if units match.
|
||||
const sourceMeta = syncCursorRegistry.getMetadata(syncKey);
|
||||
yCursorEl.style.display =
|
||||
sourceMeta?.yAxisUnit === syncMetadata?.yAxisUnit ? '' : 'none';
|
||||
}
|
||||
sync: { key: syncKey, scales: ['x', null] },
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import type { TooltipSyncMetadata } from './types';
|
||||
|
||||
/**
|
||||
* Module-level registry that tracks the metadata of the panel currently
|
||||
* acting as the cursor source (the one being hovered) per sync group.
|
||||
*
|
||||
* uPlot fires the source panel's setCursor hook before broadcasting to
|
||||
* receivers, so the registry is always populated before receivers read it.
|
||||
*
|
||||
* Receivers use this to make decisions such as:
|
||||
* - Whether to show the horizontal crosshair line (matching yAxisUnit)
|
||||
* - Future: what to render inside the tooltip (matching groupBy, etc.)
|
||||
*/
|
||||
const metadataBySyncKey = new Map<string, TooltipSyncMetadata | undefined>();
|
||||
|
||||
export const syncCursorRegistry = {
|
||||
setMetadata(syncKey: string, metadata: TooltipSyncMetadata | undefined): void {
|
||||
metadataBySyncKey.set(syncKey, metadata);
|
||||
},
|
||||
|
||||
getMetadata(syncKey: string): TooltipSyncMetadata | undefined {
|
||||
return metadataBySyncKey.get(syncKey);
|
||||
},
|
||||
};
|
||||
@@ -34,16 +34,11 @@ export interface TooltipLayoutInfo {
|
||||
height: number;
|
||||
}
|
||||
|
||||
export interface TooltipSyncMetadata {
|
||||
yAxisUnit?: string;
|
||||
}
|
||||
|
||||
export interface TooltipPluginProps {
|
||||
config: UPlotConfigBuilder;
|
||||
canPinTooltip?: boolean;
|
||||
syncMode?: DashboardCursorSync;
|
||||
syncKey?: string;
|
||||
syncMetadata?: TooltipSyncMetadata;
|
||||
render: (args: TooltipRenderArgs) => ReactNode;
|
||||
pinnedTooltipElement?: (clickData: TooltipClickData) => ReactNode;
|
||||
maxWidth?: number;
|
||||
|
||||
@@ -516,7 +516,7 @@ describe('TooltipPlugin', () => {
|
||||
);
|
||||
|
||||
expect(setCursorSpy).toHaveBeenCalledWith({
|
||||
sync: { key: 'dashboard-sync', scales: ['x', 'y'] },
|
||||
sync: { key: 'dashboard-sync', scales: ['x', null] },
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
2
go.mod
2
go.mod
@@ -8,7 +8,7 @@ require (
|
||||
github.com/ClickHouse/clickhouse-go/v2 v2.40.1
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.2
|
||||
github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd
|
||||
github.com/SigNoz/signoz-otel-collector v0.144.3
|
||||
github.com/SigNoz/signoz-otel-collector v0.144.3-rc.4
|
||||
github.com/antlr4-go/antlr/v4 v4.13.1
|
||||
github.com/antonmedv/expr v1.15.3
|
||||
github.com/cespare/xxhash/v2 v2.3.0
|
||||
|
||||
4
go.sum
4
go.sum
@@ -108,8 +108,8 @@ github.com/SigNoz/expr v1.17.7-beta h1:FyZkleM5dTQ0O6muQfwGpoH5A2ohmN/XTasRCO72g
|
||||
github.com/SigNoz/expr v1.17.7-beta/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
|
||||
github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd h1:Bk43AsDYe0fhkbj57eGXx8H3ZJ4zhmQXBnrW523ktj8=
|
||||
github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd/go.mod h1:nxRcH/OEdM8QxzH37xkGzomr1O0JpYBRS6pwjsWW6Pc=
|
||||
github.com/SigNoz/signoz-otel-collector v0.144.3 h1:/7PPIqIpPsaWtrgnfHam2XVYP41ZlgEKLHzQO8oVxcA=
|
||||
github.com/SigNoz/signoz-otel-collector v0.144.3/go.mod h1:9pLVpcIQvUT88ZiNnZN/MI+XLruvwC+Xm2UzPmGjNfA=
|
||||
github.com/SigNoz/signoz-otel-collector v0.144.3-rc.4 h1:EskJkEMfuuIyArWhV8SleDV/fuKxiaEGTXrCZIFqDT4=
|
||||
github.com/SigNoz/signoz-otel-collector v0.144.3-rc.4/go.mod h1:9pLVpcIQvUT88ZiNnZN/MI+XLruvwC+Xm2UzPmGjNfA=
|
||||
github.com/Yiling-J/theine-go v0.6.2 h1:1GeoXeQ0O0AUkiwj2S9Jc0Mzx+hpqzmqsJ4kIC4M9AY=
|
||||
github.com/Yiling-J/theine-go v0.6.2/go.mod h1:08QpMa5JZ2pKN+UJCRrCasWYO1IKCdl54Xa836rpmDU=
|
||||
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/dashboardtypes/dashboardtypesv2"
|
||||
"github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/gorilla/mux"
|
||||
@@ -137,5 +138,25 @@ func (provider *provider) addDashboardRoutes(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// TEMP dummy route to exercise the dashboard v2 schema in OpenAPI generation.
|
||||
// Handler function is not invoked at spec-gen time; only OpenAPIDef types are
|
||||
// walked. Reuses provider.dashboardHandler.Create to avoid wiring a real handler.
|
||||
if err := router.Handle("/api/v2/dashboards", handler.New(provider.authZ.AdminAccess(provider.dashboardHandler.Create), handler.OpenAPIDef{
|
||||
ID: "CreateDashboardV2",
|
||||
Tags: []string{"dashboard"},
|
||||
Summary: "Create dashboard v2",
|
||||
Description: "TEMP: dummy endpoint to exercise v2 spec shape while we iterate on plugin schema.",
|
||||
Request: new(dashboardtypesv2.DashboardData),
|
||||
RequestContentType: "application/json",
|
||||
Response: new(dashboardtypesv2.DashboardData),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusCreated,
|
||||
ErrorStatusCodes: []int{},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
})).Methods(http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -32,16 +32,26 @@ func NewModule(metadataStore telemetrytypes.MetadataStore, telemetrystore teleme
|
||||
}
|
||||
|
||||
func (m *module) ListPromotedAndIndexedPaths(ctx context.Context) ([]promotetypes.PromotePath, error) {
|
||||
indexes, err := m.metadataStore.ListLogsJSONIndexes(ctx)
|
||||
logsIndexes, err := m.metadataStore.ListLogsJSONIndexes(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Flatten the map values (which are slices) into a single slice
|
||||
indexes := slices.Concat(slices.Collect(maps.Values(logsIndexes))...)
|
||||
|
||||
aggr := map[string][]promotetypes.WrappedIndex{}
|
||||
for _, index := range indexes {
|
||||
aggr[index.Name] = append(aggr[index.Name], promotetypes.WrappedIndex{
|
||||
ColumnType: telemetrytypes.MappingFieldDataTypeToJSONDataType[index.FieldDataType].StringValue(),
|
||||
Type: index.IndexType,
|
||||
path, columnType, err := schemamigrator.UnfoldJSONSubColumnIndexExpr(index.Expression)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// clean backticks from the path
|
||||
path = strings.ReplaceAll(path, "`", "")
|
||||
|
||||
aggr[path] = append(aggr[path], promotetypes.WrappedIndex{
|
||||
ColumnType: columnType,
|
||||
Type: index.Type,
|
||||
Granularity: index.Granularity,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -204,7 +204,7 @@ func AdjustKey(key *telemetrytypes.TelemetryFieldKey, keys map[string][]*telemet
|
||||
// Downstream query builder should handle multiple matching keys with their own metadata
|
||||
// and not rely on this function to do so.
|
||||
materialized := true
|
||||
indexes := []telemetrytypes.TelemetryFieldKeySkipIndex{}
|
||||
indexes := []telemetrytypes.JSONDataTypeIndex{}
|
||||
fieldContextsSeen := map[telemetrytypes.FieldContext]bool{}
|
||||
dataTypesSeen := map[telemetrytypes.FieldDataType]bool{}
|
||||
for _, matchingKey := range matchingKeys {
|
||||
|
||||
@@ -195,8 +195,8 @@ func (c *jsonConditionBuilder) buildPrimitiveTerminalCondition(node *telemetryty
|
||||
// the field genuinely holds the empty/zero value.
|
||||
//
|
||||
// Note: indexing is also skipped for Array Nested fields because they cannot be indexed.
|
||||
indexed := slices.ContainsFunc(node.TerminalConfig.Key.Indexes, func(index telemetrytypes.TelemetryFieldKeySkipIndex) bool {
|
||||
return telemetrytypes.MappingFieldDataTypeToJSONDataType[index.FieldDataType] == node.TerminalConfig.ElemType
|
||||
indexed := slices.ContainsFunc(node.TerminalConfig.Key.Indexes, func(index telemetrytypes.JSONDataTypeIndex) bool {
|
||||
return index.Type == node.TerminalConfig.ElemType
|
||||
})
|
||||
isExistsCheck := operator == qbtypes.FilterOperatorExists || operator == qbtypes.FilterOperatorNotExists
|
||||
if node.TerminalConfig.ElemType.IndexSupported && indexed && !isExistsCheck {
|
||||
|
||||
@@ -1127,12 +1127,9 @@ func buildTestTelemetryMetadataStore(t *testing.T, addIndexes bool) *telemetryty
|
||||
return entry.Path == path && entry.Type == jsonType
|
||||
})
|
||||
if idx >= 0 {
|
||||
key.Indexes = append(key.Indexes, telemetrytypes.TelemetryFieldKeySkipIndex{
|
||||
Name: path,
|
||||
FieldContext: telemetrytypes.FieldContextBody,
|
||||
FieldDataType: fdt,
|
||||
BaseColumn: LogsV2BodyV2Column,
|
||||
IndexExpression: schemamigrator.JSONSubColumnIndexExpr(LogsV2BodyV2Column, path, jsonType.StringValue()),
|
||||
key.Indexes = append(key.Indexes, telemetrytypes.JSONDataTypeIndex{
|
||||
Type: jsonType,
|
||||
ColumnExpression: schemamigrator.JSONSubColumnIndexExpr(LogsV2BodyV2Column, path, jsonType.StringValue()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@ func (t *telemetryMetaStore) buildJSONPlans(keys []*telemetrytypes.TelemetryFiel
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *telemetryMetaStore) getJSONPathIndexes(ctx context.Context, paths ...string) (map[string][]telemetrytypes.TelemetryFieldKeySkipIndex, error) {
|
||||
func (t *telemetryMetaStore) getJSONPathIndexes(ctx context.Context, paths ...string) (map[string][]telemetrytypes.JSONDataTypeIndex, error) {
|
||||
filteredPaths := []string{}
|
||||
for _, path := range paths {
|
||||
// skip array paths; since they don't have any indexes
|
||||
@@ -116,22 +116,47 @@ func (t *telemetryMetaStore) getJSONPathIndexes(ctx context.Context, paths ...st
|
||||
filteredPaths = append(filteredPaths, path)
|
||||
}
|
||||
if len(filteredPaths) == 0 {
|
||||
return make(map[string][]telemetrytypes.TelemetryFieldKeySkipIndex), nil
|
||||
return make(map[string][]telemetrytypes.JSONDataTypeIndex), nil
|
||||
}
|
||||
|
||||
// list indexes for the paths
|
||||
indexes, err := t.ListLogsJSONIndexes(ctx, filteredPaths...)
|
||||
indexesMap, err := t.ListLogsJSONIndexes(ctx, filteredPaths...)
|
||||
if err != nil {
|
||||
return nil, errors.WrapInternalf(err, CodeFailLoadLogsJSONIndexes, "failed to list JSON path indexes")
|
||||
}
|
||||
|
||||
// build a set of indexes
|
||||
fieldPathToIndexes := make(map[string][]telemetrytypes.TelemetryFieldKeySkipIndex)
|
||||
for _, index := range indexes {
|
||||
fieldPathToIndexes[index.Name] = append(fieldPathToIndexes[index.Name], index)
|
||||
cleanIndexes := make(map[string][]telemetrytypes.JSONDataTypeIndex)
|
||||
for path, indexes := range indexesMap {
|
||||
for _, index := range indexes {
|
||||
columnExpr, columnType, err := schemamigrator.UnfoldJSONSubColumnIndexExpr(index.Expression)
|
||||
if err != nil {
|
||||
return nil, errors.WrapInternalf(err, CodeFailLoadLogsJSONIndexes, "failed to unfold JSON sub column index expression: %s", index.Expression)
|
||||
}
|
||||
|
||||
jsonDataType, found := telemetrytypes.MappingStringToJSONDataType[columnType]
|
||||
if !found {
|
||||
t.logger.ErrorContext(ctx, "failed to map column type to JSON data type", slog.String("column_type", columnType), slog.String("column_expr", columnExpr))
|
||||
continue
|
||||
}
|
||||
|
||||
if jsonDataType == telemetrytypes.String {
|
||||
cleanIndexes[path] = append(cleanIndexes[path], telemetrytypes.JSONDataTypeIndex{
|
||||
Type: telemetrytypes.String,
|
||||
ColumnExpression: columnExpr,
|
||||
IndexExpression: index.Expression,
|
||||
})
|
||||
} else if strings.HasPrefix(index.Type, "minmax") {
|
||||
cleanIndexes[path] = append(cleanIndexes[path], telemetrytypes.JSONDataTypeIndex{
|
||||
Type: jsonDataType,
|
||||
ColumnExpression: columnExpr,
|
||||
IndexExpression: index.Expression,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fieldPathToIndexes, nil
|
||||
return cleanIndexes, nil
|
||||
}
|
||||
|
||||
func buildListLogsJSONIndexesQuery(cluster string, filters ...string) (string, []any) {
|
||||
@@ -155,7 +180,7 @@ func buildListLogsJSONIndexesQuery(cluster string, filters ...string) (string, [
|
||||
return sb.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
}
|
||||
|
||||
func (t *telemetryMetaStore) ListLogsJSONIndexes(ctx context.Context, filters ...string) ([]telemetrytypes.TelemetryFieldKeySkipIndex, error) {
|
||||
func (t *telemetryMetaStore) ListLogsJSONIndexes(ctx context.Context, filters ...string) (map[string][]schemamigrator.Index, error) {
|
||||
ctx = withTelemetryContext(ctx, "ListLogsJSONIndexes")
|
||||
query, args := buildListLogsJSONIndexesQuery(t.telemetrystore.Cluster(), filters...)
|
||||
rows, err := t.telemetrystore.ClickhouseDB().Query(ctx, query, args...)
|
||||
@@ -164,7 +189,7 @@ func (t *telemetryMetaStore) ListLogsJSONIndexes(ctx context.Context, filters ..
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
indexes := []telemetrytypes.TelemetryFieldKeySkipIndex{}
|
||||
indexes := make(map[string][]schemamigrator.Index)
|
||||
for rows.Next() {
|
||||
var name string
|
||||
var typeFull string
|
||||
@@ -173,42 +198,11 @@ func (t *telemetryMetaStore) ListLogsJSONIndexes(ctx context.Context, filters ..
|
||||
if err := rows.Scan(&name, &typeFull, &expr, &granularity); err != nil {
|
||||
return nil, errors.WrapInternalf(err, CodeFailLoadLogsJSONIndexes, "failed to scan string indexed column")
|
||||
}
|
||||
|
||||
columnExpr, columnType, err := schemamigrator.UnfoldJSONSubColumnIndexExpr(expr)
|
||||
if err != nil {
|
||||
return nil, errors.WrapInternalf(err, CodeFailLoadLogsJSONIndexes, "failed to unfold JSON sub column index expression: %s", expr)
|
||||
}
|
||||
|
||||
fdt, found := telemetrytypes.MappingJSONDataTypeToFieldDataType[columnType]
|
||||
if !found {
|
||||
t.logger.ErrorContext(ctx, "failed to map JSON data type to field data type", slog.String("column_type", columnType), slog.String("column_expr", columnExpr))
|
||||
continue
|
||||
}
|
||||
|
||||
// Key by the field path (e.g. "user.name"), not by the ClickHouse index name.
|
||||
// columnExpr is the full column reference like "body_v2.user.name"; strip the
|
||||
// column prefix and any backticks added for special-character path segments.
|
||||
baseColumn := ""
|
||||
fieldName := ""
|
||||
switch {
|
||||
case strings.HasPrefix(columnExpr, telemetrylogs.BodyV2ColumnPrefix):
|
||||
baseColumn = telemetrylogs.BodyV2ColumnPrefix
|
||||
fieldName = strings.TrimPrefix(columnExpr, telemetrylogs.BodyV2ColumnPrefix)
|
||||
case strings.HasPrefix(columnExpr, telemetrylogs.BodyPromotedColumnPrefix):
|
||||
baseColumn = telemetrylogs.BodyPromotedColumnPrefix
|
||||
fieldName = strings.TrimPrefix(columnExpr, telemetrylogs.BodyPromotedColumnPrefix)
|
||||
}
|
||||
fieldName = strings.ReplaceAll(fieldName, "`", "")
|
||||
|
||||
indexes = append(indexes, telemetrytypes.TelemetryFieldKeySkipIndex{
|
||||
Name: fieldName,
|
||||
FieldContext: telemetrytypes.FieldContextBody,
|
||||
FieldDataType: fdt,
|
||||
BaseColumn: baseColumn,
|
||||
IndexName: name,
|
||||
IndexType: typeFull,
|
||||
IndexExpression: expr,
|
||||
Granularity: int(granularity),
|
||||
indexes[name] = append(indexes[name], schemamigrator.Index{
|
||||
Name: name,
|
||||
Type: typeFull,
|
||||
Expression: expr,
|
||||
Granularity: int(granularity),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,262 +0,0 @@
|
||||
package dashboardtypes
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
qb "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/go-playground/validator/v10"
|
||||
v1 "github.com/perses/perses/pkg/model/api/v1"
|
||||
"github.com/perses/perses/pkg/model/api/v1/common"
|
||||
"github.com/perses/perses/pkg/model/api/v1/dashboard"
|
||||
)
|
||||
|
||||
// StorableDashboardDataV2 wraps v1.DashboardSpec (Perses) with additional SigNoz-specific fields.
|
||||
//
|
||||
// We embed DashboardSpec (not v1.Dashboard) to avoid carrying Perses's Metadata
|
||||
// (Name, Project, CreatedAt, UpdatedAt, Tags, Version) and Kind field. SigNoz
|
||||
// manages identity (ID), timestamps (TimeAuditable), and multi-tenancy (OrgID)
|
||||
// separately on StorableDashboardV2/DashboardV2.
|
||||
//
|
||||
// The following v1 request fields map to locations inside v1.DashboardSpec:
|
||||
// - title → Display.Name (common.Display)
|
||||
// - description → Display.Description (common.Display)
|
||||
//
|
||||
// Fields that have no Perses equivalent will be added in this wrapper (like image, uploadGrafana, etc.)
|
||||
type StorableDashboardDataV2 = v1.DashboardSpec
|
||||
|
||||
// UnmarshalAndValidateDashboardV2JSON unmarshals the JSON into a StorableDashboardDataV2
|
||||
// (= PostableDashboardV2 = UpdatableDashboardV2) and validates plugin kinds and specs.
|
||||
func UnmarshalAndValidateDashboardV2JSON(data []byte) (*StorableDashboardDataV2, error) {
|
||||
var d StorableDashboardDataV2
|
||||
// Note: DashboardSpec has a custom UnmarshalJSON which prevents
|
||||
// DisallowUnknownFields from working at the top level. Unknown
|
||||
// fields in plugin specs are still rejected by validateAndNormalizePluginSpec.
|
||||
if err := json.Unmarshal(data, &d); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := validateDashboardV2(d); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &d, nil
|
||||
}
|
||||
|
||||
// Plugin kind → spec type factory. Each value is a pointer to the zero value of the
|
||||
// expected spec struct. validatePluginSpec marshals plugin.Spec back to JSON and
|
||||
// unmarshals into the typed struct to catch field-level errors.
|
||||
var (
|
||||
panelPluginSpecs = map[PanelPluginKind]func() any{
|
||||
PanelKindTimeSeries: func() any { return new(TimeSeriesPanelSpec) },
|
||||
PanelKindBarChart: func() any { return new(BarChartPanelSpec) },
|
||||
PanelKindNumber: func() any { return new(NumberPanelSpec) },
|
||||
PanelKindPieChart: func() any { return new(PieChartPanelSpec) },
|
||||
PanelKindTable: func() any { return new(TablePanelSpec) },
|
||||
PanelKindHistogram: func() any { return new(HistogramPanelSpec) },
|
||||
PanelKindList: func() any { return new(ListPanelSpec) },
|
||||
}
|
||||
queryPluginSpecs = map[QueryPluginKind]func() any{
|
||||
QueryKindBuilder: func() any { return new(BuilderQuerySpec) },
|
||||
QueryKindComposite: func() any { return new(CompositeQuerySpec) },
|
||||
QueryKindFormula: func() any { return new(FormulaSpec) },
|
||||
QueryKindPromQL: func() any { return new(PromQLQuerySpec) },
|
||||
QueryKindClickHouseSQL: func() any { return new(ClickHouseSQLQuerySpec) },
|
||||
QueryKindTraceOperator: func() any { return new(TraceOperatorSpec) },
|
||||
}
|
||||
variablePluginSpecs = map[VariablePluginKind]func() any{
|
||||
VariableKindDynamic: func() any { return new(DynamicVariableSpec) },
|
||||
VariableKindQuery: func() any { return new(QueryVariableSpec) },
|
||||
VariableKindCustom: func() any { return new(CustomVariableSpec) },
|
||||
VariableKindTextbox: func() any { return new(TextboxVariableSpec) },
|
||||
}
|
||||
datasourcePluginSpecs = map[DatasourcePluginKind]func() any{
|
||||
DatasourceKindSigNoz: func() any { return new(struct{}) },
|
||||
}
|
||||
|
||||
// allowedQueryKinds maps each panel plugin kind to the query plugin
|
||||
// kinds it supports. Composite sub-query types are mapped to these
|
||||
// same kind strings via compositeSubQueryTypeToPluginKind.
|
||||
allowedQueryKinds = map[PanelPluginKind][]QueryPluginKind{
|
||||
PanelKindTimeSeries: {QueryKindBuilder, QueryKindComposite, QueryKindFormula, QueryKindTraceOperator, QueryKindPromQL, QueryKindClickHouseSQL},
|
||||
PanelKindBarChart: {QueryKindBuilder, QueryKindComposite, QueryKindFormula, QueryKindTraceOperator, QueryKindPromQL, QueryKindClickHouseSQL},
|
||||
PanelKindNumber: {QueryKindBuilder, QueryKindComposite, QueryKindFormula, QueryKindTraceOperator, QueryKindPromQL, QueryKindClickHouseSQL},
|
||||
PanelKindHistogram: {QueryKindBuilder, QueryKindComposite, QueryKindFormula, QueryKindTraceOperator, QueryKindPromQL, QueryKindClickHouseSQL},
|
||||
PanelKindPieChart: {QueryKindBuilder, QueryKindComposite, QueryKindFormula, QueryKindTraceOperator, QueryKindClickHouseSQL},
|
||||
PanelKindTable: {QueryKindBuilder, QueryKindComposite, QueryKindFormula, QueryKindTraceOperator, QueryKindClickHouseSQL},
|
||||
PanelKindList: {QueryKindBuilder},
|
||||
}
|
||||
|
||||
// compositeSubQueryTypeToPluginKind maps CompositeQuery sub-query type
|
||||
// strings to the equivalent top-level query plugin kind for validation.
|
||||
compositeSubQueryTypeToPluginKind = map[qb.QueryType]QueryPluginKind{
|
||||
qb.QueryTypeBuilder: QueryKindBuilder,
|
||||
qb.QueryTypeFormula: QueryKindFormula,
|
||||
qb.QueryTypeTraceOperator: QueryKindTraceOperator,
|
||||
qb.QueryTypePromQL: QueryKindPromQL,
|
||||
qb.QueryTypeClickHouseSQL: QueryKindClickHouseSQL,
|
||||
}
|
||||
)
|
||||
|
||||
func validateDashboardV2(d StorableDashboardDataV2) error {
|
||||
for name, ds := range d.Datasources {
|
||||
if err := validateDatasourcePlugin(&ds.Plugin, fmt.Sprintf("spec.datasources.%s.plugin", name)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for i, v := range d.Variables {
|
||||
if err := validateVariablePlugin(v, fmt.Sprintf("spec.variables[%d]", i)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for key, panel := range d.Panels {
|
||||
if panel == nil {
|
||||
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "spec.panels.%s: panel must not be null", key)
|
||||
}
|
||||
path := fmt.Sprintf("spec.panels.%s", key)
|
||||
if err := validatePanelPlugin(&panel.Spec.Plugin, path+".spec.plugin"); err != nil {
|
||||
return err
|
||||
}
|
||||
panelKind := PanelPluginKind(panel.Spec.Plugin.Kind)
|
||||
allowed := allowedQueryKinds[panelKind]
|
||||
for qi := range panel.Spec.Queries {
|
||||
queryPath := fmt.Sprintf("%s.spec.queries[%d].spec.plugin", path, qi)
|
||||
if err := validateQueryPlugin(&panel.Spec.Queries[qi].Spec.Plugin, queryPath); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateQueryAllowedForPanel(panel.Spec.Queries[qi].Spec.Plugin, allowed, panelKind, queryPath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateDatasourcePlugin(plugin *common.Plugin, path string) error {
|
||||
kind := DatasourcePluginKind(plugin.Kind)
|
||||
factory, ok := datasourcePluginSpecs[kind]
|
||||
if !ok {
|
||||
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput,
|
||||
"%s: unknown datasource plugin kind %q; allowed values: %s", path, kind, formatEnum(kind.Enum()))
|
||||
}
|
||||
return validateAndNormalizePluginSpec(plugin, factory, path)
|
||||
}
|
||||
|
||||
func validateVariablePlugin(v dashboard.Variable, path string) error {
|
||||
switch spec := v.Spec.(type) {
|
||||
case *dashboard.ListVariableSpec:
|
||||
pluginPath := path + ".spec.plugin"
|
||||
kind := VariablePluginKind(spec.Plugin.Kind)
|
||||
factory, ok := variablePluginSpecs[kind]
|
||||
if !ok {
|
||||
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput,
|
||||
"%s: unknown variable plugin kind %q; allowed values: %s", pluginPath, kind, formatEnum(kind.Enum()))
|
||||
}
|
||||
return validateAndNormalizePluginSpec(&spec.Plugin, factory, pluginPath)
|
||||
case *dashboard.TextVariableSpec:
|
||||
// TextVariables have no plugin, nothing to validate.
|
||||
return nil
|
||||
default:
|
||||
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "%s: unsupported variable kind %q", path, v.Kind)
|
||||
}
|
||||
}
|
||||
|
||||
func validatePanelPlugin(plugin *common.Plugin, path string) error {
|
||||
kind := PanelPluginKind(plugin.Kind)
|
||||
factory, ok := panelPluginSpecs[kind]
|
||||
if !ok {
|
||||
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput,
|
||||
"%s: unknown panel plugin kind %q; allowed values: %s", path, kind, formatEnum(kind.Enum()))
|
||||
}
|
||||
return validateAndNormalizePluginSpec(plugin, factory, path)
|
||||
}
|
||||
|
||||
func validateQueryPlugin(plugin *common.Plugin, path string) error {
|
||||
kind := QueryPluginKind(plugin.Kind)
|
||||
factory, ok := queryPluginSpecs[kind]
|
||||
if !ok {
|
||||
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput,
|
||||
"%s: unknown query plugin kind %q; allowed values: %s", path, kind, formatEnum(kind.Enum()))
|
||||
}
|
||||
return validateAndNormalizePluginSpec(plugin, factory, path)
|
||||
}
|
||||
|
||||
func formatEnum(values []any) string {
|
||||
parts := make([]string, len(values))
|
||||
for i, v := range values {
|
||||
parts[i] = fmt.Sprintf("`%v`", v)
|
||||
}
|
||||
return strings.Join(parts, ", ")
|
||||
}
|
||||
|
||||
// validateAndNormalizePluginSpec validates the plugin spec and writes the typed
|
||||
// struct (with defaults) back into plugin.Spec so that DB storage and API
|
||||
// responses contain normalized values.
|
||||
func validateAndNormalizePluginSpec(plugin *common.Plugin, factory func() any, path string) error {
|
||||
if plugin.Kind == "" {
|
||||
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "%s: plugin kind is required", path)
|
||||
}
|
||||
if plugin.Spec == nil {
|
||||
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "%s: plugin spec is required", path)
|
||||
}
|
||||
// Re-marshal the spec and unmarshal into the typed struct.
|
||||
specJSON, err := json.Marshal(plugin.Spec)
|
||||
if err != nil {
|
||||
return errors.WrapInvalidInputf(err, ErrCodeDashboardInvalidInput, "%s.spec", path)
|
||||
}
|
||||
target := factory()
|
||||
decoder := json.NewDecoder(bytes.NewReader(specJSON))
|
||||
decoder.DisallowUnknownFields()
|
||||
if err := decoder.Decode(target); err != nil {
|
||||
return errors.WrapInvalidInputf(err, ErrCodeDashboardInvalidInput, "%s.spec", path)
|
||||
}
|
||||
if err := validator.New().Struct(target); err != nil {
|
||||
return errors.WrapInvalidInputf(err, ErrCodeDashboardInvalidInput, "%s.spec", path)
|
||||
}
|
||||
// Write the typed struct back so defaults are included.
|
||||
plugin.Spec = target
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateQueryAllowedForPanel checks that the query plugin kind is permitted
|
||||
// for the given panel. For composite queries it recurses into sub-queries.
|
||||
func validateQueryAllowedForPanel(plugin common.Plugin, allowed []QueryPluginKind, panelKind PanelPluginKind, path string) error {
|
||||
queryKind := QueryPluginKind(plugin.Kind)
|
||||
if !slices.Contains(allowed, queryKind) {
|
||||
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput,
|
||||
"%s: query kind %q is not supported by panel kind %q", path, queryKind, panelKind)
|
||||
}
|
||||
|
||||
// For composite queries, validate each sub-query type.
|
||||
if queryKind == QueryKindComposite && plugin.Spec != nil {
|
||||
specJSON, err := json.Marshal(plugin.Spec)
|
||||
if err != nil {
|
||||
return errors.WrapInvalidInputf(err, ErrCodeDashboardInvalidInput, "%s.spec", path)
|
||||
}
|
||||
var composite struct {
|
||||
Queries []struct {
|
||||
Type qb.QueryType `json:"type"`
|
||||
} `json:"queries"`
|
||||
}
|
||||
if err := json.Unmarshal(specJSON, &composite); err != nil {
|
||||
return errors.WrapInvalidInputf(err, ErrCodeDashboardInvalidInput, "%s.spec", path)
|
||||
}
|
||||
for si, sub := range composite.Queries {
|
||||
pluginKind, ok := compositeSubQueryTypeToPluginKind[sub.Type]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if !slices.Contains(allowed, pluginKind) {
|
||||
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput,
|
||||
"%s.spec.queries[%d]: sub-query type %q is not supported by panel kind %q",
|
||||
path, si, sub.Type, panelKind)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
274
pkg/types/dashboardtypes/dashboardtypesv2/dashboard.go
Normal file
274
pkg/types/dashboardtypes/dashboardtypesv2/dashboard.go
Normal file
@@ -0,0 +1,274 @@
|
||||
package dashboardtypesv2
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
|
||||
qb "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
v1 "github.com/perses/perses/pkg/model/api/v1"
|
||||
"github.com/perses/perses/pkg/model/api/v1/common"
|
||||
"github.com/perses/perses/pkg/model/api/v1/dashboard"
|
||||
"github.com/perses/perses/pkg/model/api/v1/variable"
|
||||
)
|
||||
|
||||
// DashboardData is the SigNoz dashboard v2 spec shape. It mirrors
|
||||
// v1.DashboardSpec (Perses) field-for-field, except every common.Plugin
|
||||
// occurrence is replaced with a typed SigNoz plugin whose OpenAPI schema is a
|
||||
// per-site discriminated oneOf.
|
||||
//
|
||||
// Drift against Perses is guarded by TestDashboardDataMatchesPerses.
|
||||
// Leaf types (common.Display, v1.Link, dashboard.Layout, variable.*) are reused
|
||||
// directly — changes in those flow through automatically, and breaking changes
|
||||
// surface as compile errors in code that uses them.
|
||||
type DashboardData struct {
|
||||
Display *common.Display `json:"display,omitempty"`
|
||||
Datasources map[string]*DatasourceSpec `json:"datasources,omitempty"`
|
||||
Variables []Variable `json:"variables,omitempty"`
|
||||
Panels map[string]*Panel `json:"panels"`
|
||||
Layouts []Layout `json:"layouts"`
|
||||
Duration common.DurationString `json:"duration"`
|
||||
RefreshInterval common.DurationString `json:"refreshInterval,omitempty"`
|
||||
Links []v1.Link `json:"links,omitempty"`
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════
|
||||
// Panel
|
||||
// ══════════════════════════════════════════════
|
||||
|
||||
type Panel struct {
|
||||
Kind string `json:"kind"`
|
||||
Spec PanelSpec `json:"spec"`
|
||||
}
|
||||
|
||||
type PanelSpec struct {
|
||||
Display *v1.PanelDisplay `json:"display,omitempty"`
|
||||
Plugin PanelPlugin `json:"plugin"`
|
||||
Queries []Query `json:"queries,omitempty"`
|
||||
Links []v1.Link `json:"links,omitempty"`
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════
|
||||
// Query
|
||||
// ══════════════════════════════════════════════
|
||||
|
||||
type Query struct {
|
||||
Kind string `json:"kind"`
|
||||
Spec QuerySpec `json:"spec"`
|
||||
}
|
||||
|
||||
type QuerySpec struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Plugin QueryPlugin `json:"plugin"`
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════
|
||||
// Variable
|
||||
// ══════════════════════════════════════════════
|
||||
|
||||
// Variable is the list/text sum type. Spec is set to *ListVariableSpec or
|
||||
// *TextVariableSpec by UnmarshalJSON based on Kind. The schema is a
|
||||
// discriminated oneOf (see JSONSchemaOneOf).
|
||||
type Variable struct {
|
||||
Kind variable.Kind `json:"kind"`
|
||||
Spec any `json:"spec"`
|
||||
}
|
||||
|
||||
// ListVariableSpec mirrors dashboard.ListVariableSpec (variable.ListSpec
|
||||
// fields + Name) but with a typed VariablePlugin replacing common.Plugin.
|
||||
type ListVariableSpec struct {
|
||||
Display *variable.Display `json:"display,omitempty"`
|
||||
DefaultValue *variable.DefaultValue `json:"defaultValue,omitempty"`
|
||||
AllowAllValue bool `json:"allowAllValue"`
|
||||
AllowMultiple bool `json:"allowMultiple"`
|
||||
CustomAllValue string `json:"customAllValue,omitempty"`
|
||||
CapturingRegexp string `json:"capturingRegexp,omitempty"`
|
||||
Sort *variable.Sort `json:"sort,omitempty"`
|
||||
Plugin VariablePlugin `json:"plugin"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// TextVariableSpec mirrors dashboard.TextVariableSpec (variable.TextSpec +
|
||||
// Name). No plugin.
|
||||
type TextVariableSpec struct {
|
||||
Display *variable.Display `json:"display,omitempty"`
|
||||
Value string `json:"value"`
|
||||
Constant bool `json:"constant,omitempty"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════
|
||||
// Layout
|
||||
// ══════════════════════════════════════════════
|
||||
|
||||
// Layout is the dashboard layout sum type. Spec is populated by UnmarshalJSON
|
||||
// with the concrete layout spec struct (today only dashboard.GridLayoutSpec)
|
||||
// based on Kind. No plugin is involved, so we reuse the Perses spec types as
|
||||
// leaf imports.
|
||||
type Layout struct {
|
||||
Kind dashboard.LayoutKind `json:"kind"`
|
||||
Spec any `json:"spec"`
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════
|
||||
// Datasource
|
||||
// ══════════════════════════════════════════════
|
||||
|
||||
type DatasourceSpec struct {
|
||||
Display *common.Display `json:"display,omitempty"`
|
||||
Default bool `json:"default"`
|
||||
Plugin DatasourcePlugin `json:"plugin"`
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════
|
||||
// Unmarshal + validate entry point
|
||||
// ══════════════════════════════════════════════
|
||||
|
||||
// UnmarshalAndValidateJSON unmarshals the JSON into a
|
||||
// DashboardData and validates cross-field rules. Plugin kind and
|
||||
// plugin-spec shape are already enforced by the typed plugin UnmarshalJSON
|
||||
// paths, so only rules that can't be expressed in the type system run here.
|
||||
func UnmarshalAndValidateJSON(data []byte) (*DashboardData, error) {
|
||||
dec := json.NewDecoder(bytes.NewReader(data))
|
||||
dec.DisallowUnknownFields()
|
||||
var d DashboardData
|
||||
if err := dec.Decode(&d); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := validateDashboard(d); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &d, nil
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════
|
||||
// Plugin kind → spec factory maps
|
||||
// ══════════════════════════════════════════════
|
||||
|
||||
// Each plugin's UnmarshalJSON uses its factory map to instantiate a typed
|
||||
// Spec based on the Kind field. Single source of truth for "which kinds
|
||||
// exist" — JSONSchemaOneOf iterates the same maps.
|
||||
var (
|
||||
panelPluginSpecs = map[PanelPluginKind]func() any{
|
||||
PanelKindTimeSeries: func() any { return new(TimeSeriesPanelSpec) },
|
||||
PanelKindBarChart: func() any { return new(BarChartPanelSpec) },
|
||||
PanelKindNumber: func() any { return new(NumberPanelSpec) },
|
||||
PanelKindPieChart: func() any { return new(PieChartPanelSpec) },
|
||||
PanelKindTable: func() any { return new(TablePanelSpec) },
|
||||
PanelKindHistogram: func() any { return new(HistogramPanelSpec) },
|
||||
PanelKindList: func() any { return new(ListPanelSpec) },
|
||||
}
|
||||
queryPluginSpecs = map[QueryPluginKind]func() any{
|
||||
QueryKindBuilder: func() any { return new(BuilderQuerySpec) },
|
||||
QueryKindComposite: func() any { return new(CompositeQuerySpec) },
|
||||
QueryKindFormula: func() any { return new(FormulaSpec) },
|
||||
QueryKindPromQL: func() any { return new(PromQLQuerySpec) },
|
||||
QueryKindClickHouseSQL: func() any { return new(ClickHouseSQLQuerySpec) },
|
||||
QueryKindTraceOperator: func() any { return new(TraceOperatorSpec) },
|
||||
}
|
||||
variablePluginSpecs = map[VariablePluginKind]func() any{
|
||||
VariableKindDynamic: func() any { return new(DynamicVariableSpec) },
|
||||
VariableKindQuery: func() any { return new(QueryVariableSpec) },
|
||||
VariableKindCustom: func() any { return new(CustomVariableSpec) },
|
||||
VariableKindTextbox: func() any { return new(TextboxVariableSpec) },
|
||||
}
|
||||
datasourcePluginSpecs = map[DatasourcePluginKind]func() any{
|
||||
DatasourceKindSigNoz: func() any { return new(struct{}) },
|
||||
}
|
||||
|
||||
// layoutSpecs is the layout sum type factory. Perses only defines
|
||||
// KindGridLayout today; adding a new kind upstream surfaces as an
|
||||
// "unknown layout kind" runtime error here until we add it.
|
||||
layoutSpecs = map[dashboard.LayoutKind]func() any{
|
||||
dashboard.KindGridLayout: func() any { return new(dashboard.GridLayoutSpec) },
|
||||
}
|
||||
|
||||
// allowedQueryKinds maps each panel plugin kind to the query plugin
|
||||
// kinds it supports. Composite sub-query types are mapped to these
|
||||
// same kind strings via compositeSubQueryTypeToPluginKind.
|
||||
allowedQueryKinds = map[PanelPluginKind][]QueryPluginKind{
|
||||
PanelKindTimeSeries: {QueryKindBuilder, QueryKindComposite, QueryKindFormula, QueryKindTraceOperator, QueryKindPromQL, QueryKindClickHouseSQL},
|
||||
PanelKindBarChart: {QueryKindBuilder, QueryKindComposite, QueryKindFormula, QueryKindTraceOperator, QueryKindPromQL, QueryKindClickHouseSQL},
|
||||
PanelKindNumber: {QueryKindBuilder, QueryKindComposite, QueryKindFormula, QueryKindTraceOperator, QueryKindPromQL, QueryKindClickHouseSQL},
|
||||
PanelKindHistogram: {QueryKindBuilder, QueryKindComposite, QueryKindFormula, QueryKindTraceOperator, QueryKindPromQL, QueryKindClickHouseSQL},
|
||||
PanelKindPieChart: {QueryKindBuilder, QueryKindComposite, QueryKindFormula, QueryKindTraceOperator, QueryKindClickHouseSQL},
|
||||
PanelKindTable: {QueryKindBuilder, QueryKindComposite, QueryKindFormula, QueryKindTraceOperator, QueryKindClickHouseSQL},
|
||||
PanelKindList: {QueryKindBuilder},
|
||||
}
|
||||
|
||||
// compositeSubQueryTypeToPluginKind maps CompositeQuery sub-query type
|
||||
// strings to the equivalent top-level query plugin kind for validation.
|
||||
compositeSubQueryTypeToPluginKind = map[qb.QueryType]QueryPluginKind{
|
||||
qb.QueryTypeBuilder: QueryKindBuilder,
|
||||
qb.QueryTypeFormula: QueryKindFormula,
|
||||
qb.QueryTypeTraceOperator: QueryKindTraceOperator,
|
||||
qb.QueryTypePromQL: QueryKindPromQL,
|
||||
qb.QueryTypeClickHouseSQL: QueryKindClickHouseSQL,
|
||||
}
|
||||
)
|
||||
|
||||
// ══════════════════════════════════════════════
|
||||
// Cross-field validation
|
||||
// ══════════════════════════════════════════════
|
||||
|
||||
func validateDashboard(d DashboardData) error {
|
||||
for key, panel := range d.Panels {
|
||||
if panel == nil {
|
||||
return errors.NewInvalidInputf(dashboardtypes.ErrCodeDashboardInvalidInput, "spec.panels.%s: panel must not be null", key)
|
||||
}
|
||||
path := fmt.Sprintf("spec.panels.%s", key)
|
||||
panelKind := panel.Spec.Plugin.Kind
|
||||
allowed := allowedQueryKinds[panelKind]
|
||||
for qi, q := range panel.Spec.Queries {
|
||||
queryPath := fmt.Sprintf("%s.spec.queries[%d].spec.plugin", path, qi)
|
||||
if err := validateQueryAllowedForPanel(q.Spec.Plugin, allowed, panelKind, queryPath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateQueryAllowedForPanel checks that the query plugin kind is permitted
|
||||
// for the given panel. For composite queries it recurses into sub-queries.
|
||||
func validateQueryAllowedForPanel(plugin QueryPlugin, allowed []QueryPluginKind, panelKind PanelPluginKind, path string) error {
|
||||
if !slices.Contains(allowed, plugin.Kind) {
|
||||
return errors.NewInvalidInputf(dashboardtypes.ErrCodeDashboardInvalidInput,
|
||||
"%s: query kind %q is not supported by panel kind %q", path, plugin.Kind, panelKind)
|
||||
}
|
||||
|
||||
if plugin.Kind != QueryKindComposite {
|
||||
return nil
|
||||
}
|
||||
composite, ok := plugin.Spec.(*CompositeQuerySpec)
|
||||
if !ok || composite == nil {
|
||||
return nil
|
||||
}
|
||||
specJSON, err := json.Marshal(composite)
|
||||
if err != nil {
|
||||
return errors.WrapInvalidInputf(err, dashboardtypes.ErrCodeDashboardInvalidInput, "%s.spec", path)
|
||||
}
|
||||
var subs struct {
|
||||
Queries []struct {
|
||||
Type qb.QueryType `json:"type"`
|
||||
} `json:"queries"`
|
||||
}
|
||||
if err := json.Unmarshal(specJSON, &subs); err != nil {
|
||||
return errors.WrapInvalidInputf(err, dashboardtypes.ErrCodeDashboardInvalidInput, "%s.spec", path)
|
||||
}
|
||||
for si, sub := range subs.Queries {
|
||||
subKind, ok := compositeSubQueryTypeToPluginKind[sub.Type]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if !slices.Contains(allowed, subKind) {
|
||||
return errors.NewInvalidInputf(dashboardtypes.ErrCodeDashboardInvalidInput,
|
||||
"%s.spec.queries[%d]: sub-query type %q is not supported by panel kind %q",
|
||||
path, si, sub.Type, panelKind)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package dashboardtypes
|
||||
package dashboardtypesv2
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -14,26 +14,26 @@ import (
|
||||
func TestValidateBigExample(t *testing.T) {
|
||||
data, err := os.ReadFile("testdata/perses.json")
|
||||
require.NoError(t, err, "reading example file")
|
||||
_, err = UnmarshalAndValidateDashboardV2JSON(data)
|
||||
_, err = UnmarshalAndValidateJSON(data)
|
||||
require.NoError(t, err, "expected valid dashboard")
|
||||
}
|
||||
|
||||
func TestValidateDashboardWithSections(t *testing.T) {
|
||||
data, err := os.ReadFile("testdata/perses_with_sections.json")
|
||||
require.NoError(t, err, "reading example file")
|
||||
_, err = UnmarshalAndValidateDashboardV2JSON(data)
|
||||
_, err = UnmarshalAndValidateJSON(data)
|
||||
require.NoError(t, err, "expected valid dashboard")
|
||||
}
|
||||
|
||||
func TestInvalidateNotAJSON(t *testing.T) {
|
||||
_, err := UnmarshalAndValidateDashboardV2JSON([]byte("not json"))
|
||||
_, err := UnmarshalAndValidateJSON([]byte("not json"))
|
||||
require.Error(t, err, "expected error for invalid JSON")
|
||||
}
|
||||
|
||||
func TestValidateEmptySpec(t *testing.T) {
|
||||
// no variables no panels
|
||||
data := []byte(`{}`)
|
||||
_, err := UnmarshalAndValidateDashboardV2JSON(data)
|
||||
_, err := UnmarshalAndValidateJSON(data)
|
||||
require.NoError(t, err, "expected valid")
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ func TestValidateOnlyVariables(t *testing.T) {
|
||||
],
|
||||
"layouts": []
|
||||
}`)
|
||||
_, err := UnmarshalAndValidateDashboardV2JSON(data)
|
||||
_, err := UnmarshalAndValidateJSON(data)
|
||||
require.NoError(t, err, "expected valid")
|
||||
}
|
||||
|
||||
@@ -148,7 +148,7 @@ func TestInvalidateUnknownPluginKind(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := UnmarshalAndValidateDashboardV2JSON([]byte(tt.data))
|
||||
_, err := UnmarshalAndValidateJSON([]byte(tt.data))
|
||||
require.Error(t, err, "expected error containing %q, got nil", tt.wantContain)
|
||||
require.Contains(t, err.Error(), tt.wantContain, "error should mention %q", tt.wantContain)
|
||||
})
|
||||
@@ -169,7 +169,7 @@ func TestInvalidateOneInvalidPanel(t *testing.T) {
|
||||
},
|
||||
"layouts": []
|
||||
}`)
|
||||
_, err := UnmarshalAndValidateDashboardV2JSON(data)
|
||||
_, err := UnmarshalAndValidateJSON(data)
|
||||
require.Error(t, err, "expected error for invalid panel plugin kind")
|
||||
require.Contains(t, err.Error(), "FakePanel", "error should mention FakePanel")
|
||||
}
|
||||
@@ -245,7 +245,7 @@ func TestRejectUnknownFieldsInPluginSpec(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := UnmarshalAndValidateDashboardV2JSON([]byte(tt.data))
|
||||
_, err := UnmarshalAndValidateJSON([]byte(tt.data))
|
||||
require.Error(t, err, "expected error for unknown field")
|
||||
require.Contains(t, err.Error(), tt.wantContain, "error should mention %q", tt.wantContain)
|
||||
})
|
||||
@@ -323,7 +323,7 @@ func TestInvalidateWrongFieldTypeInPluginSpec(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := UnmarshalAndValidateDashboardV2JSON([]byte(tt.data))
|
||||
_, err := UnmarshalAndValidateJSON([]byte(tt.data))
|
||||
require.Error(t, err, "expected validation error")
|
||||
if tt.wantContain != "" {
|
||||
require.Contains(t, err.Error(), tt.wantContain, "error should mention %q", tt.wantContain)
|
||||
@@ -531,7 +531,7 @@ func TestInvalidateBadPanelSpecValues(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := UnmarshalAndValidateDashboardV2JSON([]byte(tt.data))
|
||||
_, err := UnmarshalAndValidateJSON([]byte(tt.data))
|
||||
require.Error(t, err, "expected error containing %q, got nil", tt.wantContain)
|
||||
require.Contains(t, err.Error(), tt.wantContain, "error should mention %q", tt.wantContain)
|
||||
})
|
||||
@@ -626,7 +626,7 @@ func TestValidateRequiredFields(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := UnmarshalAndValidateDashboardV2JSON([]byte(tt.data))
|
||||
_, err := UnmarshalAndValidateJSON([]byte(tt.data))
|
||||
require.Error(t, err, "expected error containing %q, got nil", tt.wantContain)
|
||||
require.Contains(t, err.Error(), tt.wantContain, "error should mention %q", tt.wantContain)
|
||||
})
|
||||
@@ -648,7 +648,7 @@ func TestTimeSeriesPanelDefaults(t *testing.T) {
|
||||
},
|
||||
"layouts": []
|
||||
}`)
|
||||
d, err := UnmarshalAndValidateDashboardV2JSON(data)
|
||||
d, err := UnmarshalAndValidateJSON(data)
|
||||
require.NoError(t, err, "unmarshal and validate failed")
|
||||
|
||||
// After validation+normalization, the plugin spec should be a typed struct.
|
||||
@@ -695,7 +695,7 @@ func TestNumberPanelDefaults(t *testing.T) {
|
||||
},
|
||||
"layouts": []
|
||||
}`)
|
||||
d, err := UnmarshalAndValidateDashboardV2JSON(data)
|
||||
d, err := UnmarshalAndValidateJSON(data)
|
||||
require.NoError(t, err, "unmarshal and validate failed")
|
||||
|
||||
require.IsType(t, &NumberPanelSpec{}, d.Panels["p1"].Spec.Plugin.Spec)
|
||||
@@ -745,7 +745,7 @@ func TestStorageRoundTrip(t *testing.T) {
|
||||
}`)
|
||||
|
||||
// Step 1: Unmarshal + validate + normalize (what the API handler does).
|
||||
d, err := UnmarshalAndValidateDashboardV2JSON(input)
|
||||
d, err := UnmarshalAndValidateJSON(input)
|
||||
require.NoError(t, err, "unmarshal and validate failed")
|
||||
|
||||
// Step 1.5: Verify struct fields have correct defaults (extra validation before storing).
|
||||
@@ -765,7 +765,7 @@ func TestStorageRoundTrip(t *testing.T) {
|
||||
require.NoError(t, err, "marshal for storage failed")
|
||||
|
||||
// Step 3: Unmarshal from JSON (simulates reading from DB).
|
||||
loaded, err := UnmarshalAndValidateDashboardV2JSON(stored)
|
||||
loaded, err := UnmarshalAndValidateJSON(stored)
|
||||
require.NoError(t, err, "unmarshal from storage failed")
|
||||
|
||||
// Step 3.5: Verify struct fields have correct defaults after loading (before returning in API).
|
||||
@@ -878,7 +878,7 @@ func TestPanelTypeQueryTypeCompatibility(t *testing.T) {
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
_, err := UnmarshalAndValidateDashboardV2JSON(tc.data)
|
||||
_, err := UnmarshalAndValidateJSON(tc.data)
|
||||
if tc.wantErr {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
106
pkg/types/dashboardtypes/dashboardtypesv2/drift_test.go
Normal file
106
pkg/types/dashboardtypes/dashboardtypesv2/drift_test.go
Normal file
@@ -0,0 +1,106 @@
|
||||
package dashboardtypesv2
|
||||
|
||||
// TestDashboardDataMatchesPerses asserts that DashboardData
|
||||
// and every nested SigNoz-owned type cover the JSON field set of their Perses
|
||||
// counterpart. It fails loud if Perses adds, renames, or removes a field
|
||||
// upstream — turning silent drift into a CI signal on the next Perses bump.
|
||||
//
|
||||
// The test does NOT check field *types* (plugin fields intentionally diverge:
|
||||
// our typed plugins vs Perses's common.Plugin). It only checks that every
|
||||
// json-tagged field in the Perses struct exists in ours under the same tag.
|
||||
//
|
||||
// Wrapper types we re-derive (variable.ListSpec is flattened into our
|
||||
// ListVariableSpec, same for TextSpec) are compared against the flattened
|
||||
// field set.
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
v1 "github.com/perses/perses/pkg/model/api/v1"
|
||||
"github.com/perses/perses/pkg/model/api/v1/dashboard"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDashboardDataMatchesPerses(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
ours reflect.Type
|
||||
perses reflect.Type
|
||||
}{
|
||||
{"DashboardSpec", typeOf[DashboardData](), typeOf[v1.DashboardSpec]()},
|
||||
{"PanelSpec", typeOf[PanelSpec](), typeOf[v1.PanelSpec]()},
|
||||
{"QuerySpec", typeOf[QuerySpec](), typeOf[v1.QuerySpec]()},
|
||||
{"DatasourceSpec", typeOf[DatasourceSpec](), typeOf[v1.DatasourceSpec]()},
|
||||
// ListVariableSpec/TextVariableSpec embed variable.ListSpec/TextSpec
|
||||
// plus a Name field. We flatten the Perses shape to compare.
|
||||
{"ListVariableSpec", typeOf[ListVariableSpec](), typeOf[dashboard.ListVariableSpec]()},
|
||||
{"TextVariableSpec", typeOf[TextVariableSpec](), typeOf[dashboard.TextVariableSpec]()},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
ours := jsonFields(c.ours)
|
||||
perses := jsonFields(c.perses)
|
||||
|
||||
missing := sortedDiff(perses, ours)
|
||||
extra := sortedDiff(ours, perses)
|
||||
|
||||
assert.Empty(t, missing,
|
||||
"DashboardData (%s) is missing json fields present on Perses %s — upstream likely added or renamed a field",
|
||||
c.ours.Name(), c.perses.Name())
|
||||
assert.Empty(t, extra,
|
||||
"DashboardData (%s) has json fields absent on Perses %s — upstream likely removed a field or we added one without the counterpart",
|
||||
c.ours.Name(), c.perses.Name())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// jsonFields returns the set of json tag names for a struct, flattening
|
||||
// anonymous embedded fields (matching encoding/json behavior).
|
||||
func jsonFields(t reflect.Type) map[string]struct{} {
|
||||
out := map[string]struct{}{}
|
||||
if t.Kind() != reflect.Struct {
|
||||
return out
|
||||
}
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
f := t.Field(i)
|
||||
// Skip unexported fields (e.g., dashboard.TextVariableSpec has an
|
||||
// unexported `variableSpec` interface tag).
|
||||
if !f.IsExported() && !f.Anonymous {
|
||||
continue
|
||||
}
|
||||
tag := f.Tag.Get("json")
|
||||
name := strings.Split(tag, ",")[0]
|
||||
// Anonymous embed with empty json name (no tag, or `json:",inline"` /
|
||||
// `json:",omitempty"`-style options-only tag) is flattened by encoding/json.
|
||||
if f.Anonymous && name == "" {
|
||||
for k := range jsonFields(f.Type) {
|
||||
out[k] = struct{}{}
|
||||
}
|
||||
continue
|
||||
}
|
||||
if tag == "-" || name == "" {
|
||||
continue
|
||||
}
|
||||
out[name] = struct{}{}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// sortedDiff returns keys in a but not in b, sorted.
|
||||
func sortedDiff(a, b map[string]struct{}) []string {
|
||||
var diff []string
|
||||
for k := range a {
|
||||
if _, ok := b[k]; !ok {
|
||||
diff = append(diff, k)
|
||||
}
|
||||
}
|
||||
sort.Strings(diff)
|
||||
return diff
|
||||
}
|
||||
|
||||
func typeOf[T any]() reflect.Type { return reflect.TypeOf((*T)(nil)).Elem() }
|
||||
@@ -1,13 +1,15 @@
|
||||
package dashboardtypes
|
||||
package dashboardtypesv2
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
|
||||
qb "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/swaggest/jsonschema-go"
|
||||
)
|
||||
|
||||
// ══════════════════════════════════════════════
|
||||
@@ -81,12 +83,28 @@ type BuilderQuerySpec struct {
|
||||
func (b *BuilderQuerySpec) UnmarshalJSON(data []byte) error {
|
||||
spec, err := qb.UnmarshalBuilderQueryBySignal(data)
|
||||
if err != nil {
|
||||
return errors.WrapInvalidInputf(err, ErrCodeDashboardInvalidInput, "invalid builder query spec")
|
||||
return errors.WrapInvalidInputf(err, dashboardtypes.ErrCodeDashboardInvalidInput, "invalid builder query spec")
|
||||
}
|
||||
b.Spec = spec
|
||||
return nil
|
||||
}
|
||||
|
||||
// PrepareJSONSchema drops the reflected struct shape so only the
|
||||
// JSONSchemaOneOf result binds.
|
||||
func (BuilderQuerySpec) PrepareJSONSchema(s *jsonschema.Schema) error {
|
||||
return clearOneOfParentShape(s)
|
||||
}
|
||||
|
||||
// JSONSchemaOneOf exposes the three signal-dispatched shapes a builder query
|
||||
// can take. Mirrors qb.UnmarshalBuilderQueryBySignal's runtime dispatch.
|
||||
func (BuilderQuerySpec) JSONSchemaOneOf() []any {
|
||||
return []any{
|
||||
qb.QueryBuilderQuery[qb.LogAggregation]{},
|
||||
qb.QueryBuilderQuery[qb.MetricAggregation]{},
|
||||
qb.QueryBuilderQuery[qb.TraceAggregation]{},
|
||||
}
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════
|
||||
// SigNoz panel plugin specs
|
||||
// ══════════════════════════════════════════════
|
||||
@@ -272,7 +290,7 @@ func (t TimePreference) MarshalJSON() ([]byte, error) {
|
||||
func (t *TimePreference) UnmarshalJSON(data []byte) error {
|
||||
var v string
|
||||
if err := json.Unmarshal(data, &v); err != nil {
|
||||
return errors.WrapInvalidInputf(err, ErrCodeDashboardInvalidInput, "invalid timePreference: must be a string, one of `global_time`, `last_5_min`, `last_15_min`, `last_30_min`, `last_1_hr`, `last_6_hr`, `last_1_day`, `last_3_days`, `last_1_week`, or `last_1_month`")
|
||||
return errors.WrapInvalidInputf(err, dashboardtypes.ErrCodeDashboardInvalidInput, "invalid timePreference: must be a string, one of `global_time`, `last_5_min`, `last_15_min`, `last_30_min`, `last_1_hr`, `last_6_hr`, `last_1_day`, `last_3_days`, `last_1_week`, or `last_1_month`")
|
||||
}
|
||||
if v == "" {
|
||||
*t = TimePreferenceGlobalTime
|
||||
@@ -284,7 +302,7 @@ func (t *TimePreference) UnmarshalJSON(data []byte) error {
|
||||
*t = tp
|
||||
return nil
|
||||
default:
|
||||
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "invalid timePreference %q: must be `global_time`, `last_5_min`, `last_15_min`, `last_30_min`, `last_1_hr`, `last_6_hr`, `last_1_day`, `last_3_days`, `last_1_week`, or `last_1_month`", v)
|
||||
return errors.NewInvalidInputf(dashboardtypes.ErrCodeDashboardInvalidInput, "invalid timePreference %q: must be `global_time`, `last_5_min`, `last_15_min`, `last_30_min`, `last_1_hr`, `last_6_hr`, `last_1_day`, `last_3_days`, `last_1_week`, or `last_1_month`", v)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -313,7 +331,7 @@ func (l LegendPosition) MarshalJSON() ([]byte, error) {
|
||||
func (l *LegendPosition) UnmarshalJSON(data []byte) error {
|
||||
var v string
|
||||
if err := json.Unmarshal(data, &v); err != nil {
|
||||
return errors.WrapInvalidInputf(err, ErrCodeDashboardInvalidInput, "invalid legend position: must be a string, one of `bottom` or `right`")
|
||||
return errors.WrapInvalidInputf(err, dashboardtypes.ErrCodeDashboardInvalidInput, "invalid legend position: must be a string, one of `bottom` or `right`")
|
||||
}
|
||||
if v == "" {
|
||||
*l = LegendPositionBottom
|
||||
@@ -325,7 +343,7 @@ func (l *LegendPosition) UnmarshalJSON(data []byte) error {
|
||||
*l = lp
|
||||
return nil
|
||||
default:
|
||||
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "invalid legend position %q: must be `bottom` or `right`", v)
|
||||
return errors.NewInvalidInputf(dashboardtypes.ErrCodeDashboardInvalidInput, "invalid legend position %q: must be `bottom` or `right`", v)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -354,7 +372,7 @@ func (f ThresholdFormat) MarshalJSON() ([]byte, error) {
|
||||
func (f *ThresholdFormat) UnmarshalJSON(data []byte) error {
|
||||
var v string
|
||||
if err := json.Unmarshal(data, &v); err != nil {
|
||||
return errors.WrapInvalidInputf(err, ErrCodeDashboardInvalidInput, "invalid threshold format: must be a string, one of `text` or `background`")
|
||||
return errors.WrapInvalidInputf(err, dashboardtypes.ErrCodeDashboardInvalidInput, "invalid threshold format: must be a string, one of `text` or `background`")
|
||||
}
|
||||
if v == "" {
|
||||
*f = ThresholdFormatText
|
||||
@@ -366,7 +384,7 @@ func (f *ThresholdFormat) UnmarshalJSON(data []byte) error {
|
||||
*f = tf
|
||||
return nil
|
||||
default:
|
||||
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "invalid threshold format %q: must be `text` or `background`", v)
|
||||
return errors.NewInvalidInputf(dashboardtypes.ErrCodeDashboardInvalidInput, "invalid threshold format %q: must be `text` or `background`", v)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -406,7 +424,7 @@ func (o ComparisonOperator) MarshalJSON() ([]byte, error) {
|
||||
func (o *ComparisonOperator) UnmarshalJSON(data []byte) error {
|
||||
var v string
|
||||
if err := json.Unmarshal(data, &v); err != nil {
|
||||
return errors.WrapInvalidInputf(err, ErrCodeDashboardInvalidInput, "invalid comparison operator: must be a string, one of `>`, `<`, `>=`, `<=`, `=`, `above`, `below`, `above_or_equal`, `below_or_equal`, `equal`, or `not_equal`")
|
||||
return errors.WrapInvalidInputf(err, dashboardtypes.ErrCodeDashboardInvalidInput, "invalid comparison operator: must be a string, one of `>`, `<`, `>=`, `<=`, `=`, `above`, `below`, `above_or_equal`, `below_or_equal`, `equal`, or `not_equal`")
|
||||
}
|
||||
if v == "" {
|
||||
*o = ComparisonOperatorGT
|
||||
@@ -420,7 +438,7 @@ func (o *ComparisonOperator) UnmarshalJSON(data []byte) error {
|
||||
*o = co
|
||||
return nil
|
||||
default:
|
||||
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "invalid comparison operator %q: must be `>`, `<`, `>=`, `<=`, `=`, `above`, `below`, `above_or_equal`, `below_or_equal`, `equal`, or `not_equal`", v)
|
||||
return errors.NewInvalidInputf(dashboardtypes.ErrCodeDashboardInvalidInput, "invalid comparison operator %q: must be `>`, `<`, `>=`, `<=`, `=`, `above`, `below`, `above_or_equal`, `below_or_equal`, `equal`, or `not_equal`", v)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -451,7 +469,7 @@ func (li LineInterpolation) MarshalJSON() ([]byte, error) {
|
||||
func (li *LineInterpolation) UnmarshalJSON(data []byte) error {
|
||||
var v string
|
||||
if err := json.Unmarshal(data, &v); err != nil {
|
||||
return errors.WrapInvalidInputf(err, ErrCodeDashboardInvalidInput, "invalid line interpolation: must be a string, one of `linear`, `spline`, `step_after`, or `step_before`")
|
||||
return errors.WrapInvalidInputf(err, dashboardtypes.ErrCodeDashboardInvalidInput, "invalid line interpolation: must be a string, one of `linear`, `spline`, `step_after`, or `step_before`")
|
||||
}
|
||||
if v == "" {
|
||||
*li = LineInterpolationSpline
|
||||
@@ -463,7 +481,7 @@ func (li *LineInterpolation) UnmarshalJSON(data []byte) error {
|
||||
*li = val
|
||||
return nil
|
||||
default:
|
||||
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "invalid line interpolation %q: must be `linear`, `spline`, `step_after`, or `step_before`", v)
|
||||
return errors.NewInvalidInputf(dashboardtypes.ErrCodeDashboardInvalidInput, "invalid line interpolation %q: must be `linear`, `spline`, `step_after`, or `step_before`", v)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -492,7 +510,7 @@ func (ls LineStyle) MarshalJSON() ([]byte, error) {
|
||||
func (ls *LineStyle) UnmarshalJSON(data []byte) error {
|
||||
var v string
|
||||
if err := json.Unmarshal(data, &v); err != nil {
|
||||
return errors.WrapInvalidInputf(err, ErrCodeDashboardInvalidInput, "invalid line style: must be a string, one of `solid` or `dashed`")
|
||||
return errors.WrapInvalidInputf(err, dashboardtypes.ErrCodeDashboardInvalidInput, "invalid line style: must be a string, one of `solid` or `dashed`")
|
||||
}
|
||||
if v == "" {
|
||||
*ls = LineStyleSolid
|
||||
@@ -504,7 +522,7 @@ func (ls *LineStyle) UnmarshalJSON(data []byte) error {
|
||||
*ls = val
|
||||
return nil
|
||||
default:
|
||||
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "invalid line style %q: must be `solid` or `dashed`", v)
|
||||
return errors.NewInvalidInputf(dashboardtypes.ErrCodeDashboardInvalidInput, "invalid line style %q: must be `solid` or `dashed`", v)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -534,7 +552,7 @@ func (fm FillMode) MarshalJSON() ([]byte, error) {
|
||||
func (fm *FillMode) UnmarshalJSON(data []byte) error {
|
||||
var v string
|
||||
if err := json.Unmarshal(data, &v); err != nil {
|
||||
return errors.WrapInvalidInputf(err, ErrCodeDashboardInvalidInput, "invalid fill mode: must be a string, one of `solid`, `gradient`, or `none`")
|
||||
return errors.WrapInvalidInputf(err, dashboardtypes.ErrCodeDashboardInvalidInput, "invalid fill mode: must be a string, one of `solid`, `gradient`, or `none`")
|
||||
}
|
||||
if v == "" {
|
||||
*fm = FillModeSolid
|
||||
@@ -546,7 +564,7 @@ func (fm *FillMode) UnmarshalJSON(data []byte) error {
|
||||
*fm = val
|
||||
return nil
|
||||
default:
|
||||
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "invalid fill mode %q: must be `solid`, `gradient`, or `none`", v)
|
||||
return errors.NewInvalidInputf(dashboardtypes.ErrCodeDashboardInvalidInput, "invalid fill mode %q: must be `solid`, `gradient`, or `none`", v)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -593,12 +611,12 @@ func (p *PrecisionOption) UnmarshalJSON(data []byte) error {
|
||||
p.String = valuer.NewString(strconv.Itoa(n))
|
||||
return nil
|
||||
default:
|
||||
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "invalid precision option %d: must be `0`, `1`, `2`, `3`, `4`, or `full`", n)
|
||||
return errors.NewInvalidInputf(dashboardtypes.ErrCodeDashboardInvalidInput, "invalid precision option %d: must be `0`, `1`, `2`, `3`, `4`, or `full`", n)
|
||||
}
|
||||
}
|
||||
var v string
|
||||
if err := json.Unmarshal(data, &v); err != nil {
|
||||
return errors.WrapInvalidInputf(err, ErrCodeDashboardInvalidInput, "invalid precision option: must be `0`, `1`, `2`, `3`, `4`, or `full`")
|
||||
return errors.WrapInvalidInputf(err, dashboardtypes.ErrCodeDashboardInvalidInput, "invalid precision option: must be `0`, `1`, `2`, `3`, `4`, or `full`")
|
||||
}
|
||||
if v == "" {
|
||||
*p = PrecisionOption2
|
||||
@@ -610,6 +628,6 @@ func (p *PrecisionOption) UnmarshalJSON(data []byte) error {
|
||||
*p = val
|
||||
return nil
|
||||
default:
|
||||
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "invalid precision option %q: must be `0`, `1`, `2`, `3`, `4`, or `full`", v)
|
||||
return errors.NewInvalidInputf(dashboardtypes.ErrCodeDashboardInvalidInput, "invalid precision option %q: must be `0`, `1`, `2`, `3`, `4`, or `full`", v)
|
||||
}
|
||||
}
|
||||
388
pkg/types/dashboardtypes/dashboardtypesv2/plugins.go
Normal file
388
pkg/types/dashboardtypes/dashboardtypesv2/plugins.go
Normal file
@@ -0,0 +1,388 @@
|
||||
package dashboardtypesv2
|
||||
|
||||
// Typed plugin envelopes for the four plugin sites in DashboardData.
|
||||
// Each plugin:
|
||||
// - Has Kind (typed enum) and Spec (any, resolved at runtime via UnmarshalJSON)
|
||||
// - Implements JSONSchemaOneOf so the reflector emits a per-site discriminated
|
||||
// oneOf over only the kinds valid at that site
|
||||
// - Implements UnmarshalJSON that dispatches Spec to the concrete type based
|
||||
// on Kind, using the factory maps in dashboard_v2.go
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/perses/perses/pkg/model/api/v1/dashboard"
|
||||
"github.com/perses/perses/pkg/model/api/v1/variable"
|
||||
"github.com/swaggest/jsonschema-go"
|
||||
)
|
||||
|
||||
// ══════════════════════════════════════════════
|
||||
// Panel plugin
|
||||
// ══════════════════════════════════════════════
|
||||
|
||||
type PanelPlugin struct {
|
||||
Kind PanelPluginKind `json:"kind"`
|
||||
Spec any `json:"spec"`
|
||||
}
|
||||
|
||||
// PrepareJSONSchema drops the reflected struct shape (type: object, properties)
|
||||
// from the envelope so that only the JSONSchemaOneOf result binds. Mirrors the
|
||||
// pattern in swaggest/jsonschema-go's built-in oneOf helper.
|
||||
func (PanelPlugin) PrepareJSONSchema(s *jsonschema.Schema) error {
|
||||
return clearOneOfParentShape(s)
|
||||
}
|
||||
|
||||
func (PanelPlugin) JSONSchemaOneOf() []any {
|
||||
return []any{
|
||||
panelPluginVariant[TimeSeriesPanelSpec]{Kind: string(PanelKindTimeSeries)},
|
||||
panelPluginVariant[BarChartPanelSpec]{Kind: string(PanelKindBarChart)},
|
||||
panelPluginVariant[NumberPanelSpec]{Kind: string(PanelKindNumber)},
|
||||
panelPluginVariant[PieChartPanelSpec]{Kind: string(PanelKindPieChart)},
|
||||
panelPluginVariant[TablePanelSpec]{Kind: string(PanelKindTable)},
|
||||
panelPluginVariant[HistogramPanelSpec]{Kind: string(PanelKindHistogram)},
|
||||
panelPluginVariant[ListPanelSpec]{Kind: string(PanelKindList)},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PanelPlugin) UnmarshalJSON(data []byte) error {
|
||||
kind, specJSON, err := splitKindSpec(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
factory, ok := panelPluginSpecs[PanelPluginKind(kind)]
|
||||
if !ok {
|
||||
return errors.NewInvalidInputf(dashboardtypes.ErrCodeDashboardInvalidInput, "unknown panel plugin kind %q", kind)
|
||||
}
|
||||
spec, err := decodePluginSpec(specJSON, factory())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.Kind = PanelPluginKind(kind)
|
||||
p.Spec = spec
|
||||
return nil
|
||||
}
|
||||
|
||||
type panelPluginVariant[S any] struct {
|
||||
Kind string `json:"kind" required:"true"`
|
||||
Spec S `json:"spec" required:"true"`
|
||||
}
|
||||
|
||||
func (v panelPluginVariant[S]) PrepareJSONSchema(s *jsonschema.Schema) error {
|
||||
return applyKindEnum(s, v.Kind)
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════
|
||||
// Query plugin
|
||||
// ══════════════════════════════════════════════
|
||||
|
||||
type QueryPlugin struct {
|
||||
Kind QueryPluginKind `json:"kind"`
|
||||
Spec any `json:"spec"`
|
||||
}
|
||||
|
||||
func (QueryPlugin) PrepareJSONSchema(s *jsonschema.Schema) error {
|
||||
return clearOneOfParentShape(s)
|
||||
}
|
||||
|
||||
func (QueryPlugin) JSONSchemaOneOf() []any {
|
||||
return []any{
|
||||
queryPluginVariant[BuilderQuerySpec]{Kind: string(QueryKindBuilder)},
|
||||
queryPluginVariant[CompositeQuerySpec]{Kind: string(QueryKindComposite)},
|
||||
queryPluginVariant[FormulaSpec]{Kind: string(QueryKindFormula)},
|
||||
queryPluginVariant[PromQLQuerySpec]{Kind: string(QueryKindPromQL)},
|
||||
queryPluginVariant[ClickHouseSQLQuerySpec]{Kind: string(QueryKindClickHouseSQL)},
|
||||
queryPluginVariant[TraceOperatorSpec]{Kind: string(QueryKindTraceOperator)},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *QueryPlugin) UnmarshalJSON(data []byte) error {
|
||||
kind, specJSON, err := splitKindSpec(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
factory, ok := queryPluginSpecs[QueryPluginKind(kind)]
|
||||
if !ok {
|
||||
return errors.NewInvalidInputf(dashboardtypes.ErrCodeDashboardInvalidInput, "unknown query plugin kind %q", kind)
|
||||
}
|
||||
spec, err := decodePluginSpec(specJSON, factory())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.Kind = QueryPluginKind(kind)
|
||||
p.Spec = spec
|
||||
return nil
|
||||
}
|
||||
|
||||
type queryPluginVariant[S any] struct {
|
||||
Kind string `json:"kind" required:"true"`
|
||||
Spec S `json:"spec" required:"true"`
|
||||
}
|
||||
|
||||
func (v queryPluginVariant[S]) PrepareJSONSchema(s *jsonschema.Schema) error {
|
||||
return applyKindEnum(s, v.Kind)
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════
|
||||
// Variable plugin
|
||||
// ══════════════════════════════════════════════
|
||||
|
||||
type VariablePlugin struct {
|
||||
Kind VariablePluginKind `json:"kind"`
|
||||
Spec any `json:"spec"`
|
||||
}
|
||||
|
||||
func (VariablePlugin) PrepareJSONSchema(s *jsonschema.Schema) error {
|
||||
return clearOneOfParentShape(s)
|
||||
}
|
||||
|
||||
func (VariablePlugin) JSONSchemaOneOf() []any {
|
||||
return []any{
|
||||
variablePluginVariant[DynamicVariableSpec]{Kind: string(VariableKindDynamic)},
|
||||
variablePluginVariant[QueryVariableSpec]{Kind: string(VariableKindQuery)},
|
||||
variablePluginVariant[CustomVariableSpec]{Kind: string(VariableKindCustom)},
|
||||
variablePluginVariant[TextboxVariableSpec]{Kind: string(VariableKindTextbox)},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *VariablePlugin) UnmarshalJSON(data []byte) error {
|
||||
kind, specJSON, err := splitKindSpec(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
factory, ok := variablePluginSpecs[VariablePluginKind(kind)]
|
||||
if !ok {
|
||||
return errors.NewInvalidInputf(dashboardtypes.ErrCodeDashboardInvalidInput, "unknown variable plugin kind %q", kind)
|
||||
}
|
||||
spec, err := decodePluginSpec(specJSON, factory())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.Kind = VariablePluginKind(kind)
|
||||
p.Spec = spec
|
||||
return nil
|
||||
}
|
||||
|
||||
type variablePluginVariant[S any] struct {
|
||||
Kind string `json:"kind" required:"true"`
|
||||
Spec S `json:"spec" required:"true"`
|
||||
}
|
||||
|
||||
func (v variablePluginVariant[S]) PrepareJSONSchema(s *jsonschema.Schema) error {
|
||||
return applyKindEnum(s, v.Kind)
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════
|
||||
// Datasource plugin
|
||||
// ══════════════════════════════════════════════
|
||||
|
||||
type DatasourcePlugin struct {
|
||||
Kind DatasourcePluginKind `json:"kind"`
|
||||
Spec any `json:"spec"`
|
||||
}
|
||||
|
||||
func (DatasourcePlugin) PrepareJSONSchema(s *jsonschema.Schema) error {
|
||||
return clearOneOfParentShape(s)
|
||||
}
|
||||
|
||||
func (DatasourcePlugin) JSONSchemaOneOf() []any {
|
||||
return []any{
|
||||
datasourcePluginVariant[struct{}]{Kind: string(DatasourceKindSigNoz)},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *DatasourcePlugin) UnmarshalJSON(data []byte) error {
|
||||
kind, specJSON, err := splitKindSpec(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
factory, ok := datasourcePluginSpecs[DatasourcePluginKind(kind)]
|
||||
if !ok {
|
||||
return errors.NewInvalidInputf(dashboardtypes.ErrCodeDashboardInvalidInput, "unknown datasource plugin kind %q", kind)
|
||||
}
|
||||
spec, err := decodePluginSpec(specJSON, factory())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.Kind = DatasourcePluginKind(kind)
|
||||
p.Spec = spec
|
||||
return nil
|
||||
}
|
||||
|
||||
type datasourcePluginVariant[S any] struct {
|
||||
Kind string `json:"kind" required:"true"`
|
||||
Spec S `json:"spec" required:"true"`
|
||||
}
|
||||
|
||||
func (v datasourcePluginVariant[S]) PrepareJSONSchema(s *jsonschema.Schema) error {
|
||||
return applyKindEnum(s, v.Kind)
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════
|
||||
// Variable envelope (list/text sum type)
|
||||
// ══════════════════════════════════════════════
|
||||
|
||||
func (Variable) PrepareJSONSchema(s *jsonschema.Schema) error {
|
||||
return clearOneOfParentShape(s)
|
||||
}
|
||||
|
||||
func (Variable) JSONSchemaOneOf() []any {
|
||||
return []any{
|
||||
variableEnvelope[ListVariableSpec]{Kind: string(variable.KindList)},
|
||||
variableEnvelope[TextVariableSpec]{Kind: string(variable.KindText)},
|
||||
}
|
||||
}
|
||||
|
||||
func (v *Variable) UnmarshalJSON(data []byte) error {
|
||||
kind, specJSON, err := splitKindSpec(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch kind {
|
||||
case string(variable.KindList):
|
||||
spec, err := decodeVariableSpec(specJSON, new(ListVariableSpec))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
v.Kind = variable.KindList
|
||||
v.Spec = spec
|
||||
case string(variable.KindText):
|
||||
spec, err := decodeVariableSpec(specJSON, new(TextVariableSpec))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
v.Kind = variable.KindText
|
||||
v.Spec = spec
|
||||
default:
|
||||
return errors.NewInvalidInputf(dashboardtypes.ErrCodeDashboardInvalidInput, "unknown variable kind %q", kind)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type variableEnvelope[S any] struct {
|
||||
Kind string `json:"kind" required:"true"`
|
||||
Spec S `json:"spec" required:"true"`
|
||||
}
|
||||
|
||||
func (v variableEnvelope[S]) PrepareJSONSchema(s *jsonschema.Schema) error {
|
||||
return applyKindEnum(s, v.Kind)
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════
|
||||
// Layout envelope (grid, potentially more in the future)
|
||||
// ══════════════════════════════════════════════
|
||||
|
||||
func (Layout) PrepareJSONSchema(s *jsonschema.Schema) error {
|
||||
return clearOneOfParentShape(s)
|
||||
}
|
||||
|
||||
func (Layout) JSONSchemaOneOf() []any {
|
||||
return []any{
|
||||
layoutEnvelope[dashboard.GridLayoutSpec]{Kind: string(dashboard.KindGridLayout)},
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Layout) UnmarshalJSON(data []byte) error {
|
||||
kind, specJSON, err := splitKindSpec(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
factory, ok := layoutSpecs[dashboard.LayoutKind(kind)]
|
||||
if !ok {
|
||||
return errors.NewInvalidInputf(dashboardtypes.ErrCodeDashboardInvalidInput, "unknown layout kind %q", kind)
|
||||
}
|
||||
spec, err := decodeVariableSpec(specJSON, factory())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
l.Kind = dashboard.LayoutKind(kind)
|
||||
l.Spec = spec
|
||||
return nil
|
||||
}
|
||||
|
||||
type layoutEnvelope[S any] struct {
|
||||
Kind string `json:"kind" required:"true"`
|
||||
Spec S `json:"spec" required:"true"`
|
||||
}
|
||||
|
||||
func (v layoutEnvelope[S]) PrepareJSONSchema(s *jsonschema.Schema) error {
|
||||
return applyKindEnum(s, v.Kind)
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════
|
||||
// Helpers
|
||||
// ══════════════════════════════════════════════
|
||||
|
||||
// splitKindSpec parses a {"kind": "...", "spec": {...}} envelope and returns
|
||||
// kind and the raw spec bytes for typed decoding.
|
||||
func splitKindSpec(data []byte) (string, []byte, error) {
|
||||
var head struct {
|
||||
Kind string `json:"kind"`
|
||||
Spec json.RawMessage `json:"spec"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &head); err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
return head.Kind, head.Spec, nil
|
||||
}
|
||||
|
||||
// decodePluginSpec strict-decodes a plugin spec JSON into target and runs
|
||||
// struct-tag validation (go-playground/validator). Strictness matches the
|
||||
// previous validateAndNormalizePluginSpec contract: unknown fields rejected,
|
||||
// required-field violations surfaced. Returns target on success.
|
||||
func decodePluginSpec(specJSON []byte, target any) (any, error) {
|
||||
if len(specJSON) == 0 {
|
||||
return nil, errors.NewInvalidInputf(dashboardtypes.ErrCodeDashboardInvalidInput, "spec is required")
|
||||
}
|
||||
dec := json.NewDecoder(bytes.NewReader(specJSON))
|
||||
dec.DisallowUnknownFields()
|
||||
if err := dec.Decode(target); err != nil {
|
||||
return nil, fmt.Errorf("decode spec: %w", err)
|
||||
}
|
||||
if err := validator.New().Struct(target); err != nil {
|
||||
return nil, fmt.Errorf("validate spec: %w", err)
|
||||
}
|
||||
return target, nil
|
||||
}
|
||||
|
||||
// decodeVariableSpec is lenient: used for ListVariableSpec / TextVariableSpec
|
||||
// where Perses historically ignored unknown fields (e.g., stray `plugin` on
|
||||
// text variables in existing fixtures).
|
||||
func decodeVariableSpec(specJSON []byte, target any) (any, error) {
|
||||
if len(specJSON) == 0 {
|
||||
return nil, errors.NewInvalidInputf(dashboardtypes.ErrCodeDashboardInvalidInput, "spec is required")
|
||||
}
|
||||
if err := json.Unmarshal(specJSON, target); err != nil {
|
||||
return nil, fmt.Errorf("decode spec: %w", err)
|
||||
}
|
||||
return target, nil
|
||||
}
|
||||
|
||||
// clearOneOfParentShape drops Type and Properties on a schema that also has a
|
||||
// JSONSchemaOneOf. Swaggest emits both the reflected struct shape and the
|
||||
// oneOf side by side; OAS-wise the oneOf is the binding constraint so the
|
||||
// properties/type are redundant noise. This mirrors swaggest's own built-in
|
||||
// oneOf helper's PrepareJSONSchema.
|
||||
func clearOneOfParentShape(s *jsonschema.Schema) error {
|
||||
s.Type = nil
|
||||
s.Properties = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// applyKindEnum narrows the `kind` property of a oneOf variant schema to a
|
||||
// single permitted string, producing `kind: { type: string, enum: [kind] }`.
|
||||
// Each variant calls this from PrepareJSONSchema because Go generics can't
|
||||
// propagate struct tag values (so we can't write enum:"..." on Kind).
|
||||
func applyKindEnum(schema *jsonschema.Schema, kind string) error {
|
||||
kindProp, ok := schema.Properties["kind"]
|
||||
if !ok || kindProp.TypeObject == nil {
|
||||
return fmt.Errorf("variant schema missing `kind` property")
|
||||
}
|
||||
kindProp.TypeObject.WithEnum(kind)
|
||||
schema.Properties["kind"] = kindProp
|
||||
return nil
|
||||
}
|
||||
@@ -60,7 +60,7 @@ func (i *PromotePath) ValidateAndSetDefaults() error {
|
||||
return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "index granularity must be greater than 0")
|
||||
}
|
||||
|
||||
jsonDataType, ok := telemetrytypes.MappingFieldDataTypeToJSONDataType[telemetrytypes.MappingJSONDataTypeToFieldDataType[index.ColumnType]]
|
||||
jsonDataType, ok := telemetrytypes.MappingStringToJSONDataType[index.ColumnType]
|
||||
if !ok {
|
||||
return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "invalid column type: %s", index.ColumnType)
|
||||
}
|
||||
|
||||
@@ -37,9 +37,9 @@ type TelemetryFieldKey struct {
|
||||
FieldContext FieldContext `json:"fieldContext,omitzero"`
|
||||
FieldDataType FieldDataType `json:"fieldDataType,omitzero"`
|
||||
|
||||
JSONPlan JSONAccessPlan `json:"-"`
|
||||
Indexes []TelemetryFieldKeySkipIndex `json:"-"`
|
||||
Materialized bool `json:"-"` // refers to promoted in case of body.... fields
|
||||
JSONPlan JSONAccessPlan `json:"-"`
|
||||
Indexes []JSONDataTypeIndex `json:"-"`
|
||||
Materialized bool `json:"-"` // refers to promoted in case of body.... fields
|
||||
|
||||
Evolutions []*EvolutionEntry `json:"-"`
|
||||
}
|
||||
@@ -102,7 +102,7 @@ func (f TelemetryFieldKey) String() string {
|
||||
if i > 0 {
|
||||
sb.WriteString("; ")
|
||||
}
|
||||
fmt.Fprintf(&sb, "{type=%s, indexExpr=%s}", MappingFieldDataTypeToJSONDataType[index.FieldDataType].StringValue(), index.IndexExpression)
|
||||
fmt.Fprintf(&sb, "{type=%s, columnExpr=%s, indexExpr=%s}", index.Type.StringValue(), index.ColumnExpression, index.IndexExpression)
|
||||
}
|
||||
sb.WriteString("]")
|
||||
}
|
||||
@@ -400,14 +400,3 @@ func NewFieldValueSelectorFromPostableFieldValueParams(params PostableFieldValue
|
||||
|
||||
return fieldValueSelector
|
||||
}
|
||||
|
||||
type TelemetryFieldKeySkipIndex struct {
|
||||
Name string `json:"name"` // Name is TelemetryFieldKey.Name not IndexName from ClickHouse
|
||||
FieldContext FieldContext `json:"fieldContext,omitzero"`
|
||||
FieldDataType FieldDataType `json:"fieldDataType,omitzero"`
|
||||
BaseColumn string `json:"baseColumn"`
|
||||
IndexName string `json:"indexName"`
|
||||
IndexType string `json:"indexType"`
|
||||
IndexExpression string `json:"indexExpression"`
|
||||
Granularity int `json:"granularity"`
|
||||
}
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
package telemetrytypes
|
||||
|
||||
type JSONDataTypeIndex struct {
|
||||
Type JSONDataType
|
||||
ColumnExpression string
|
||||
IndexExpression string
|
||||
}
|
||||
|
||||
type JSONDataType struct {
|
||||
str string // Store the correct case for ClickHouse
|
||||
IsArray bool
|
||||
@@ -26,17 +32,18 @@ var (
|
||||
ArrayJSON = JSONDataType{"Array(JSON)", true, "JSON", false}
|
||||
)
|
||||
|
||||
var MappingJSONDataTypeToFieldDataType = map[string]FieldDataType{
|
||||
"String": FieldDataTypeString,
|
||||
"Int64": FieldDataTypeInt64,
|
||||
"Float64": FieldDataTypeFloat64,
|
||||
"Bool": FieldDataTypeBool,
|
||||
"Array(Nullable(String))": FieldDataTypeArrayString,
|
||||
"Array(Nullable(Int64))": FieldDataTypeArrayInt64,
|
||||
"Array(Nullable(Float64))": FieldDataTypeArrayFloat64,
|
||||
"Array(Nullable(Bool))": FieldDataTypeArrayBool,
|
||||
"Array(Dynamic)": FieldDataTypeArrayDynamic,
|
||||
"Array(JSON)": FieldDataTypeArrayJSON,
|
||||
var MappingStringToJSONDataType = map[string]JSONDataType{
|
||||
"String": String,
|
||||
"Int64": Int64,
|
||||
"Float64": Float64,
|
||||
"Bool": Bool,
|
||||
"Dynamic": Dynamic,
|
||||
"Array(Nullable(String))": ArrayString,
|
||||
"Array(Nullable(Int64))": ArrayInt64,
|
||||
"Array(Nullable(Float64))": ArrayFloat64,
|
||||
"Array(Nullable(Bool))": ArrayBool,
|
||||
"Array(Dynamic)": ArrayDynamic,
|
||||
"Array(JSON)": ArrayJSON,
|
||||
}
|
||||
|
||||
var MappingFieldDataTypeToJSONDataType = map[FieldDataType]JSONDataType{
|
||||
|
||||
@@ -3,6 +3,7 @@ package telemetrytypes
|
||||
import (
|
||||
"context"
|
||||
|
||||
schemamigrator "github.com/SigNoz/signoz-otel-collector/cmd/signozschemamigrator/schema_migrator"
|
||||
"github.com/SigNoz/signoz/pkg/types/metrictypes"
|
||||
)
|
||||
|
||||
@@ -34,7 +35,7 @@ type MetadataStore interface {
|
||||
FetchTemporalityAndTypeMulti(ctx context.Context, queryTimeRangeStartTs, queryTimeRangeEndTs uint64, metricNames ...string) (map[string]metrictypes.Temporality, map[string]metrictypes.Type, error)
|
||||
|
||||
// ListLogsJSONIndexes lists the JSON indexes for the logs table.
|
||||
ListLogsJSONIndexes(ctx context.Context, filters ...string) ([]TelemetryFieldKeySkipIndex, error)
|
||||
ListLogsJSONIndexes(ctx context.Context, filters ...string) (map[string][]schemamigrator.Index, error)
|
||||
|
||||
// ListPromotedPaths lists the promoted paths.
|
||||
GetPromotedPaths(ctx context.Context, paths ...string) (map[string]bool, error)
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
schemamigrator "github.com/SigNoz/signoz-otel-collector/cmd/signozschemamigrator/schema_migrator"
|
||||
"github.com/SigNoz/signoz/pkg/types/metrictypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
)
|
||||
@@ -17,7 +18,7 @@ type MockMetadataStore struct {
|
||||
TemporalityMap map[string]metrictypes.Temporality
|
||||
TypeMap map[string]metrictypes.Type
|
||||
PromotedPathsMap map[string]bool
|
||||
LogsJSONIndexes []telemetrytypes.TelemetryFieldKeySkipIndex
|
||||
LogsJSONIndexesMap map[string][]schemamigrator.Index
|
||||
ColumnEvolutionMetadataMap map[string][]*telemetrytypes.EvolutionEntry
|
||||
LookupKeysMap map[telemetrytypes.MetricMetadataLookupKey]int64
|
||||
// StaticFields holds signal-specific intrinsic field definitions (e.g. telemetrylogs.IntrinsicFields).
|
||||
@@ -33,7 +34,7 @@ func NewMockMetadataStore() *MockMetadataStore {
|
||||
TemporalityMap: make(map[string]metrictypes.Temporality),
|
||||
TypeMap: make(map[string]metrictypes.Type),
|
||||
PromotedPathsMap: make(map[string]bool),
|
||||
LogsJSONIndexes: []telemetrytypes.TelemetryFieldKeySkipIndex{},
|
||||
LogsJSONIndexesMap: make(map[string][]schemamigrator.Index),
|
||||
ColumnEvolutionMetadataMap: make(map[string][]*telemetrytypes.EvolutionEntry),
|
||||
LookupKeysMap: make(map[telemetrytypes.MetricMetadataLookupKey]int64),
|
||||
StaticFields: make(map[string]telemetrytypes.TelemetryFieldKey),
|
||||
@@ -368,8 +369,8 @@ func (m *MockMetadataStore) GetPromotedPaths(ctx context.Context, paths ...strin
|
||||
}
|
||||
|
||||
// ListLogsJSONIndexes lists the JSON indexes for the logs table.
|
||||
func (m *MockMetadataStore) ListLogsJSONIndexes(ctx context.Context, filters ...string) ([]telemetrytypes.TelemetryFieldKeySkipIndex, error) {
|
||||
return m.LogsJSONIndexes, nil
|
||||
func (m *MockMetadataStore) ListLogsJSONIndexes(ctx context.Context, filters ...string) (map[string][]schemamigrator.Index, error) {
|
||||
return m.LogsJSONIndexesMap, nil
|
||||
}
|
||||
|
||||
func (m *MockMetadataStore) updateColumnEvolutionMetadataForKeys(_ context.Context, keysToUpdate []*telemetrytypes.TelemetryFieldKey) map[string][]*telemetrytypes.EvolutionEntry {
|
||||
|
||||
@@ -23,7 +23,6 @@ pytest_plugins = [
|
||||
"fixtures.notification_channel",
|
||||
"fixtures.alerts",
|
||||
"fixtures.cloudintegrations",
|
||||
"fixtures.jsontypeexporter",
|
||||
]
|
||||
|
||||
|
||||
@@ -79,6 +78,6 @@ def pytest_addoption(parser: pytest.Parser):
|
||||
parser.addoption(
|
||||
"--schema-migrator-version",
|
||||
action="store",
|
||||
default="v0.144.3",
|
||||
default="v0.144.2",
|
||||
help="schema migrator version",
|
||||
)
|
||||
|
||||
@@ -1,473 +0,0 @@
|
||||
"""
|
||||
Simpler version of jsontypeexporter for test fixtures.
|
||||
This exports JSON type metadata to the path_types table by parsing JSON bodies
|
||||
and extracting all paths with their types, similar to how the real jsontypeexporter works.
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import json
|
||||
from abc import ABC
|
||||
from http import HTTPStatus
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
Dict,
|
||||
Generator,
|
||||
List,
|
||||
Optional,
|
||||
Set,
|
||||
Union,
|
||||
)
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
from fixtures import types
|
||||
|
||||
|
||||
class JSONPathType(ABC):
|
||||
"""Represents a JSON path with its type information"""
|
||||
|
||||
field_name: str
|
||||
field_data_type: str
|
||||
last_seen: np.uint64
|
||||
signal: str = "logs"
|
||||
field_context: str = "body"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
field_name: str,
|
||||
field_data_type: str,
|
||||
last_seen: Optional[datetime.datetime] = None,
|
||||
) -> None:
|
||||
self.field_name = field_name
|
||||
self.field_data_type = field_data_type
|
||||
self.signal = "logs"
|
||||
self.field_context = "body"
|
||||
if last_seen is None:
|
||||
last_seen = datetime.datetime.now()
|
||||
self.last_seen = np.uint64(int(last_seen.timestamp() * 1e9))
|
||||
|
||||
def np_arr(self) -> np.array:
|
||||
"""Return path type data as numpy array for database insertion"""
|
||||
return np.array([self.signal, self.field_context, self.field_name, self.field_data_type, self.last_seen])
|
||||
|
||||
|
||||
# Constants matching jsontypeexporter
|
||||
ARRAY_SEPARATOR = "[]." # Used in paths like "education[].name"
|
||||
ARRAY_SUFFIX = "[]" # Used when traversing into array element objects
|
||||
|
||||
|
||||
def _infer_array_type_from_type_strings(types: List[str]) -> Optional[str]:
|
||||
"""
|
||||
Infer array type from a list of pre-classified type strings.
|
||||
Matches metadataexporter's inferArrayMask logic.
|
||||
|
||||
Internal type strings are: "JSON", "String", "Bool", "Float64", "Int64"
|
||||
|
||||
SuperTyping rules (matching Go inferArrayMask):
|
||||
- JSON alone → []json
|
||||
- JSON + any primitive → []dynamic
|
||||
- String alone → []string; String + other → []dynamic
|
||||
- Float64 wins over Int64 and Bool
|
||||
- Int64 wins over Bool
|
||||
- Bool alone → []bool
|
||||
"""
|
||||
if len(types) == 0:
|
||||
return None
|
||||
|
||||
unique = set(types)
|
||||
|
||||
has_json = "JSON" in unique
|
||||
# hasPrimitive mirrors Go: (hasJSON && len(unique) > 1) || (!hasJSON && len(unique) > 0)
|
||||
has_primitive = (has_json and len(unique) > 1) or (not has_json and len(unique) > 0)
|
||||
|
||||
if has_json:
|
||||
if not has_primitive:
|
||||
return "[]json"
|
||||
return "[]dynamic"
|
||||
|
||||
# ---- Primitive Type Resolution (Float > Int > Bool) ----
|
||||
if "String" in unique:
|
||||
if len(unique) > 1:
|
||||
return "[]dynamic"
|
||||
return "[]string"
|
||||
|
||||
if "Float64" in unique:
|
||||
return "[]float64"
|
||||
if "Int64" in unique:
|
||||
return "[]int64"
|
||||
if "Bool" in unique:
|
||||
return "[]bool"
|
||||
|
||||
return "[]dynamic"
|
||||
|
||||
|
||||
def _infer_array_type(elements: List[Any]) -> Optional[str]:
|
||||
"""
|
||||
Infer array type from raw Python list elements.
|
||||
Classifies each element then delegates to _infer_array_type_from_type_strings.
|
||||
"""
|
||||
if len(elements) == 0:
|
||||
return None
|
||||
|
||||
types = []
|
||||
for elem in elements:
|
||||
if elem is None:
|
||||
continue
|
||||
if isinstance(elem, dict):
|
||||
types.append("JSON")
|
||||
elif isinstance(elem, str):
|
||||
types.append("String")
|
||||
elif isinstance(elem, bool): # must be before int (bool is subclass of int)
|
||||
types.append("Bool")
|
||||
elif isinstance(elem, float):
|
||||
types.append("Float64")
|
||||
elif isinstance(elem, int):
|
||||
types.append("Int64")
|
||||
|
||||
return _infer_array_type_from_type_strings(types)
|
||||
|
||||
|
||||
def _python_type_to_clickhouse_type(value: Any) -> str:
|
||||
"""
|
||||
Convert Python type to ClickHouse JSON type string.
|
||||
Maps Python types to ClickHouse JSON data types.
|
||||
Matches metadataexporter's mapPCommonValueTypeToDataType.
|
||||
"""
|
||||
if isinstance(value, bool):
|
||||
return "bool"
|
||||
elif isinstance(value, int):
|
||||
return "int64"
|
||||
elif isinstance(value, float):
|
||||
return "float64"
|
||||
elif isinstance(value, str):
|
||||
return "string"
|
||||
elif isinstance(value, list):
|
||||
# Use the sophisticated array type inference
|
||||
array_type = _infer_array_type(value)
|
||||
return array_type if array_type else "[]dynamic"
|
||||
elif isinstance(value, dict):
|
||||
return "json"
|
||||
else:
|
||||
return "string" # Default fallback
|
||||
|
||||
|
||||
def _extract_json_paths(
|
||||
obj: Any,
|
||||
current_path: str = "",
|
||||
path_types: Optional[Dict[str, Set[str]]] = None,
|
||||
level: int = 0,
|
||||
) -> Dict[str, Set[str]]:
|
||||
"""
|
||||
Recursively extract all paths and their types from a JSON object.
|
||||
Matches jsontypeexporter's analyzePValue logic.
|
||||
|
||||
Args:
|
||||
obj: The JSON object to traverse
|
||||
current_path: Current path being built (e.g., "user.name")
|
||||
path_types: Dictionary mapping paths to sets of types found
|
||||
level: Current nesting level (for depth limiting)
|
||||
|
||||
Returns:
|
||||
Dictionary mapping paths to sets of type strings
|
||||
"""
|
||||
if path_types is None:
|
||||
path_types = {}
|
||||
|
||||
if obj is None:
|
||||
# Skip null values — matches Go walkNode which errors on ValueTypeEmpty
|
||||
return path_types
|
||||
|
||||
if isinstance(obj, dict):
|
||||
# For objects, recurse into keys without recording the object itself as a type.
|
||||
# Matches Go walkMap which recurses without calling ta.record on the map node.
|
||||
|
||||
for key, value in obj.items():
|
||||
# Build the path for this key
|
||||
if current_path:
|
||||
new_path = f"{current_path}.{key}"
|
||||
else:
|
||||
new_path = key
|
||||
|
||||
# Recurse into the value
|
||||
_extract_json_paths(value, new_path, path_types, level + 1)
|
||||
|
||||
elif isinstance(obj, list):
|
||||
# Skip empty arrays
|
||||
if len(obj) == 0:
|
||||
return path_types
|
||||
|
||||
# Collect types from array elements (matching Go: types := make([]pcommon.ValueType, 0, s.Len()))
|
||||
types = []
|
||||
|
||||
for item in obj:
|
||||
if isinstance(item, dict):
|
||||
# When traversing into array element objects, use ArraySuffix ([])
|
||||
# This matches: prefix+ArraySuffix in the Go code
|
||||
# Example: if current_path is "education", we use "education[]" to traverse into objects
|
||||
array_prefix = current_path + ARRAY_SUFFIX if current_path else ""
|
||||
for key, value in item.items():
|
||||
if array_prefix:
|
||||
# Use array separator: education[].name
|
||||
array_path = f"{array_prefix}.{key}"
|
||||
else:
|
||||
array_path = key
|
||||
# Recurse without increasing level (matching Go behavior)
|
||||
_extract_json_paths(value, array_path, path_types, level)
|
||||
types.append("JSON")
|
||||
elif isinstance(item, list):
|
||||
# Arrays inside arrays are not supported - skip the whole path
|
||||
# Matching Go: e.logger.Error("arrays inside arrays are not supported!", ...); return nil
|
||||
return path_types
|
||||
elif isinstance(item, str):
|
||||
types.append("String")
|
||||
elif isinstance(item, bool):
|
||||
types.append("Bool")
|
||||
elif isinstance(item, float):
|
||||
types.append("Float64")
|
||||
elif isinstance(item, int):
|
||||
types.append("Int64")
|
||||
|
||||
# Infer array type from collected types (matching Go: if mask := inferArrayMask(types); mask != 0)
|
||||
if len(types) > 0:
|
||||
array_type = _infer_array_type_from_type_strings(types)
|
||||
if array_type and current_path:
|
||||
if current_path not in path_types:
|
||||
path_types[current_path] = set()
|
||||
path_types[current_path].add(array_type)
|
||||
|
||||
else:
|
||||
# Primitive value (string, number, bool)
|
||||
if current_path:
|
||||
if current_path not in path_types:
|
||||
path_types[current_path] = set()
|
||||
obj_type = _python_type_to_clickhouse_type(obj)
|
||||
path_types[current_path].add(obj_type)
|
||||
|
||||
return path_types
|
||||
|
||||
|
||||
def _parse_json_bodies_and_extract_paths(
|
||||
json_bodies: List[str],
|
||||
timestamp: Optional[datetime.datetime] = None,
|
||||
) -> List[JSONPathType]:
|
||||
"""
|
||||
Parse JSON bodies and extract all paths with their types.
|
||||
This mimics the behavior of jsontypeexporter.
|
||||
|
||||
Args:
|
||||
json_bodies: List of JSON body strings to parse
|
||||
timestamp: Timestamp to use for last_seen (defaults to now)
|
||||
|
||||
Returns:
|
||||
List of JSONPathType objects with all discovered paths and types
|
||||
"""
|
||||
if timestamp is None:
|
||||
timestamp = datetime.datetime.now()
|
||||
|
||||
# Aggregate all paths and their types across all JSON bodies
|
||||
all_path_types: Dict[str, Set[str]] = {}
|
||||
|
||||
for json_body in json_bodies:
|
||||
try:
|
||||
parsed = json.loads(json_body)
|
||||
_extract_json_paths(parsed, "", all_path_types, level=0)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
# Skip invalid JSON
|
||||
continue
|
||||
|
||||
# Convert to list of JSONPathType objects
|
||||
# Each path can have multiple types, so we create one JSONPathType per type
|
||||
path_type_objects: List[JSONPathType] = []
|
||||
for path, types_set in all_path_types.items():
|
||||
for type_str in types_set:
|
||||
path_type_objects.append(
|
||||
JSONPathType(field_name=path, field_data_type=type_str, last_seen=timestamp)
|
||||
)
|
||||
|
||||
return path_type_objects
|
||||
|
||||
|
||||
@pytest.fixture(name="export_json_types", scope="function")
|
||||
def export_json_types(
|
||||
clickhouse: types.TestContainerClickhouse,
|
||||
request: pytest.FixtureRequest, # To access migrator fixture
|
||||
) -> Generator[
|
||||
Callable[[Union[List[JSONPathType], List[str], List[Any]]], None], Any, None
|
||||
]:
|
||||
"""
|
||||
Fixture for exporting JSON type metadata to the path_types table.
|
||||
This is a simpler version of jsontypeexporter for test fixtures.
|
||||
|
||||
The function can accept:
|
||||
1. List of JSONPathType objects (manual specification)
|
||||
2. List of JSON body strings (auto-extract paths)
|
||||
3. List of Logs objects (extract from body_json field)
|
||||
|
||||
Usage examples:
|
||||
# Manual specification
|
||||
export_json_types([
|
||||
JSONPathType(field_name="user.name", field_data_type="string"),
|
||||
JSONPathType(field_name="user.age", field_data_type="int64"),
|
||||
])
|
||||
|
||||
# Auto-extract from JSON strings
|
||||
export_json_types([
|
||||
'{"user": {"name": "alice", "age": 25}}',
|
||||
'{"user": {"name": "bob", "age": 30}}',
|
||||
])
|
||||
|
||||
# Auto-extract from Logs objects
|
||||
export_json_types(logs_list)
|
||||
"""
|
||||
# Ensure migrator has run to create the table
|
||||
try:
|
||||
request.getfixturevalue("migrator")
|
||||
except Exception:
|
||||
# If migrator fixture is not available, that's okay - table might already exist
|
||||
pass
|
||||
|
||||
def _export_json_types(
|
||||
data: Union[
|
||||
List[JSONPathType], List[str], List[Any]
|
||||
], # List[Logs] but avoiding circular import
|
||||
) -> None:
|
||||
"""
|
||||
Export JSON type metadata to signoz_metadata.distributed_field_keys table.
|
||||
This table stores signal, context, path, and type information for body JSON fields.
|
||||
"""
|
||||
path_types: List[JSONPathType] = []
|
||||
|
||||
if len(data) == 0:
|
||||
return
|
||||
|
||||
# Determine input type and convert to JSONPathType list
|
||||
first_item = data[0]
|
||||
|
||||
if isinstance(first_item, JSONPathType):
|
||||
# Already JSONPathType objects
|
||||
path_types = data # type: ignore
|
||||
elif isinstance(first_item, str):
|
||||
# List of JSON strings - parse and extract paths
|
||||
path_types = _parse_json_bodies_and_extract_paths(data) # type: ignore
|
||||
else:
|
||||
# Assume it's a list of Logs objects - extract body_v2
|
||||
json_bodies: List[str] = []
|
||||
for log in data: # type: ignore
|
||||
# Try to get body_v2 attribute
|
||||
if hasattr(log, "body_v2") and log.body_v2:
|
||||
json_bodies.append(log.body_v2)
|
||||
elif hasattr(log, "body") and log.body:
|
||||
# Fallback to body if body_v2 not available
|
||||
try:
|
||||
# Try to parse as JSON
|
||||
json.loads(log.body)
|
||||
json_bodies.append(log.body)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
pass
|
||||
|
||||
if json_bodies:
|
||||
path_types = _parse_json_bodies_and_extract_paths(json_bodies)
|
||||
|
||||
if len(path_types) == 0:
|
||||
return
|
||||
|
||||
clickhouse.conn.insert(
|
||||
database="signoz_metadata",
|
||||
table="distributed_field_keys",
|
||||
data=[path_type.np_arr() for path_type in path_types],
|
||||
column_names=[
|
||||
"signal",
|
||||
"field_context",
|
||||
"field_name",
|
||||
"field_data_type",
|
||||
"last_seen",
|
||||
],
|
||||
)
|
||||
|
||||
yield _export_json_types
|
||||
|
||||
# Cleanup - truncate the local table after tests (following pattern from logs fixture)
|
||||
clickhouse.conn.query(
|
||||
f"TRUNCATE TABLE signoz_metadata.field_keys ON CLUSTER '{clickhouse.env['SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_CLUSTER']}' SYNC"
|
||||
)
|
||||
|
||||
@pytest.fixture(name="create_json_index", scope="function")
|
||||
def create_json_index(
|
||||
signoz: types.SigNoz,
|
||||
) -> Generator[Callable[[str, List[Dict[str, Any]]], None], None, None]:
|
||||
"""
|
||||
Create ClickHouse data-skipping indexes on body_v2 JSON sub-columns via
|
||||
POST /api/v1/logs/promote_paths.
|
||||
|
||||
**Must be called BEFORE insert_logs** so that newly inserted data parts are
|
||||
covered by the index and the QB uses the indexed condition path.
|
||||
|
||||
Each entry in `paths` follows the PromotePath API shape:
|
||||
{
|
||||
"path": "body.user.name", # must start with "body."
|
||||
"indexes": [
|
||||
{
|
||||
"column_type": "String", # String | Int64 | Float64
|
||||
"type": "ngrambf_v1(3, 256, 2, 0)", # or "minmax", "tokenbf_v1(...)"
|
||||
"granularity": 1,
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
Teardown drops every index created during the test by querying
|
||||
system.data_skipping_indices for matching expressions.
|
||||
|
||||
Example::
|
||||
|
||||
def test_foo(signoz, get_token, insert_logs, export_json_types, create_json_body_index):
|
||||
token = get_token(...)
|
||||
export_json_types(logs_list)
|
||||
create_json_body_index(token, [
|
||||
{"path": "body.user.name",
|
||||
"indexes": [{"column_type": "String", "type": "ngrambf_v1(3, 256, 2, 0)", "granularity": 1}]},
|
||||
{"path": "body.user.age",
|
||||
"indexes": [{"column_type": "Int64", "type": "minmax", "granularity": 1}]},
|
||||
])
|
||||
insert_logs(logs_list) # data inserted after index exists — index is built automatically
|
||||
"""
|
||||
created_paths: List[str] = []
|
||||
|
||||
def _create_json_body_index(token: str, paths: List[Dict[str, Any]]) -> None:
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/logs/promote_paths"),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json=paths,
|
||||
timeout=30,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.CREATED, (
|
||||
f"Failed to create JSON body indexes: "
|
||||
f"{response.status_code} {response.text}"
|
||||
)
|
||||
for path in paths:
|
||||
# The API strips the "body." prefix before storing — mirror that here
|
||||
# so our cleanup query uses the bare path (e.g. "user.name").
|
||||
raw = path["path"].removeprefix("body.")
|
||||
if raw not in created_paths:
|
||||
created_paths.append(raw)
|
||||
|
||||
yield _create_json_body_index
|
||||
|
||||
if not created_paths:
|
||||
return
|
||||
|
||||
cluster = signoz.telemetrystore.env["SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_CLUSTER"]
|
||||
for path in created_paths:
|
||||
result = signoz.telemetrystore.conn.query(
|
||||
"SELECT name FROM system.data_skipping_indices "
|
||||
"WHERE database = 'signoz_logs' AND table = 'logs_v2' "
|
||||
f"AND expr LIKE '%{path}%'"
|
||||
)
|
||||
for (index_name,) in result.result_rows:
|
||||
signoz.telemetrystore.conn.query(
|
||||
f"ALTER TABLE signoz_logs.logs_v2 "
|
||||
f"ON CLUSTER '{cluster}' "
|
||||
f"DROP INDEX IF EXISTS `{index_name}`"
|
||||
)
|
||||
@@ -122,8 +122,6 @@ class Logs(ABC):
|
||||
resources: dict[str, Any] = {},
|
||||
attributes: dict[str, Any] = {},
|
||||
body: str = "default body",
|
||||
body_v2: Optional[str] = None,
|
||||
body_promoted: Optional[str] = None,
|
||||
severity_text: str = "INFO",
|
||||
trace_id: str = "",
|
||||
span_id: str = "",
|
||||
@@ -169,33 +167,6 @@ class Logs(ABC):
|
||||
# Set body
|
||||
self.body = body
|
||||
|
||||
# Set body_v2 - if body is JSON, parse and stringify it, otherwise use empty string
|
||||
# ClickHouse accepts String input for JSON column
|
||||
if body_v2 is not None:
|
||||
self.body_v2 = body_v2
|
||||
else:
|
||||
# Try to parse body as JSON; if successful use it directly,
|
||||
# otherwise wrap as {"message": body} matching the normalize operator behavior.
|
||||
try:
|
||||
json.loads(body)
|
||||
self.body_v2 = body
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
self.body_v2 = json.dumps({"message": body})
|
||||
|
||||
# Set body_promoted - must be valid JSON
|
||||
# Tests will explicitly pass promoted column's content, but we validate it
|
||||
if body_promoted is not None:
|
||||
# Validate that it's valid JSON
|
||||
try:
|
||||
json.loads(body_promoted)
|
||||
self.body_promoted = body_promoted
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
# If invalid, default to empty JSON object
|
||||
self.body_promoted = "{}"
|
||||
else:
|
||||
# Default to empty JSON object (valid JSON)
|
||||
self.body_promoted = "{}"
|
||||
|
||||
# Process resources and attributes
|
||||
self.resources_string = {k: str(v) for k, v in resources.items()}
|
||||
self.resource_json = (
|
||||
@@ -355,8 +326,6 @@ class Logs(ABC):
|
||||
self.severity_text,
|
||||
self.severity_number,
|
||||
self.body,
|
||||
self.body_v2,
|
||||
self.body_promoted,
|
||||
self.attributes_string,
|
||||
self.attributes_number,
|
||||
self.attributes_bool,
|
||||
@@ -485,53 +454,31 @@ def insert_logs(
|
||||
data=[resource_key.np_arr() for resource_key in resource_keys],
|
||||
)
|
||||
|
||||
# All columns in insertion order (must match Logs.np_arr() order)
|
||||
all_column_names = [
|
||||
"ts_bucket_start",
|
||||
"resource_fingerprint",
|
||||
"timestamp",
|
||||
"observed_timestamp",
|
||||
"id",
|
||||
"trace_id",
|
||||
"span_id",
|
||||
"trace_flags",
|
||||
"severity_text",
|
||||
"severity_number",
|
||||
"body",
|
||||
"body_v2",
|
||||
"body_promoted",
|
||||
"attributes_string",
|
||||
"attributes_number",
|
||||
"attributes_bool",
|
||||
"resources_string",
|
||||
"scope_name",
|
||||
"scope_version",
|
||||
"scope_string",
|
||||
"resource",
|
||||
]
|
||||
|
||||
# Check if body_v2 column exists (only present when ENABLE_LOGS_MIGRATIONS_V2 migration has run)
|
||||
result = clickhouse.conn.query(
|
||||
"SELECT count() FROM system.columns WHERE database = 'signoz_logs' AND table = 'logs_v2' AND name = 'body_v2'"
|
||||
)
|
||||
has_json_body = result.result_rows[0][0] > 0
|
||||
|
||||
if has_json_body:
|
||||
column_names = all_column_names
|
||||
data = [log.np_arr() for log in logs]
|
||||
else:
|
||||
json_body_cols = {"body_v2", "body_promoted"}
|
||||
keep_indices = [
|
||||
i for i, c in enumerate(all_column_names) if c not in json_body_cols
|
||||
]
|
||||
column_names = [all_column_names[i] for i in keep_indices]
|
||||
data = [log.np_arr()[keep_indices] for log in logs]
|
||||
|
||||
clickhouse.conn.insert(
|
||||
database="signoz_logs",
|
||||
table="distributed_logs_v2",
|
||||
data=data,
|
||||
column_names=column_names,
|
||||
data=[log.np_arr() for log in logs],
|
||||
column_names=[
|
||||
"ts_bucket_start",
|
||||
"resource_fingerprint",
|
||||
"timestamp",
|
||||
"observed_timestamp",
|
||||
"id",
|
||||
"trace_id",
|
||||
"span_id",
|
||||
"trace_flags",
|
||||
"severity_text",
|
||||
"severity_number",
|
||||
"body",
|
||||
"attributes_string",
|
||||
"attributes_number",
|
||||
"attributes_bool",
|
||||
"resources_string",
|
||||
"scope_name",
|
||||
"scope_version",
|
||||
"scope_string",
|
||||
"resource",
|
||||
],
|
||||
)
|
||||
|
||||
yield _insert_logs
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from typing import Optional
|
||||
|
||||
import docker
|
||||
import pytest
|
||||
from testcontainers.core.container import Network
|
||||
@@ -10,32 +8,27 @@ from fixtures.logger import setup_logger
|
||||
logger = setup_logger(__name__)
|
||||
|
||||
|
||||
def create_migrator(
|
||||
@pytest.fixture(name="migrator", scope="package")
|
||||
def migrator(
|
||||
network: Network,
|
||||
clickhouse: types.TestContainerClickhouse,
|
||||
request: pytest.FixtureRequest,
|
||||
pytestconfig: pytest.Config,
|
||||
cache_key: str = "migrator",
|
||||
env_overrides: Optional[dict] = None,
|
||||
) -> types.Operation:
|
||||
"""
|
||||
Factory function for running schema migrations.
|
||||
Accepts optional env_overrides to customize the migrator environment.
|
||||
Package-scoped fixture for running schema migrations.
|
||||
"""
|
||||
|
||||
def create() -> None:
|
||||
version = request.config.getoption("--schema-migrator-version")
|
||||
client = docker.from_env()
|
||||
|
||||
environment = dict(env_overrides) if env_overrides else {}
|
||||
|
||||
container = client.containers.run(
|
||||
image=f"signoz/signoz-schema-migrator:{version}",
|
||||
command=f"sync --replication=true --cluster-name=cluster --up= --dsn={clickhouse.env["SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_DSN"]}",
|
||||
detach=True,
|
||||
auto_remove=False,
|
||||
network=network.id,
|
||||
environment=environment,
|
||||
)
|
||||
|
||||
result = container.wait()
|
||||
@@ -54,7 +47,6 @@ def create_migrator(
|
||||
detach=True,
|
||||
auto_remove=False,
|
||||
network=network.id,
|
||||
environment=environment,
|
||||
)
|
||||
|
||||
result = container.wait()
|
||||
@@ -67,7 +59,7 @@ def create_migrator(
|
||||
|
||||
container.remove()
|
||||
|
||||
return types.Operation(name=cache_key)
|
||||
return types.Operation(name="migrator")
|
||||
|
||||
def delete(_: types.Operation) -> None:
|
||||
pass
|
||||
@@ -78,27 +70,9 @@ def create_migrator(
|
||||
return dev.wrap(
|
||||
request,
|
||||
pytestconfig,
|
||||
cache_key,
|
||||
"migrator",
|
||||
lambda: types.Operation(name=""),
|
||||
create,
|
||||
delete,
|
||||
restore,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(name="migrator", scope="package")
|
||||
def migrator(
|
||||
network: Network,
|
||||
clickhouse: types.TestContainerClickhouse,
|
||||
request: pytest.FixtureRequest,
|
||||
pytestconfig: pytest.Config,
|
||||
) -> types.Operation:
|
||||
"""
|
||||
Package-scoped fixture for running schema migrations.
|
||||
"""
|
||||
return create_migrator(
|
||||
network=network,
|
||||
clickhouse=clickhouse,
|
||||
request=request,
|
||||
pytestconfig=pytestconfig,
|
||||
)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,70 +0,0 @@
|
||||
import pytest
|
||||
from testcontainers.core.container import Network
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.migrator import create_migrator
|
||||
from fixtures.signoz import create_signoz
|
||||
|
||||
UNSUPPORTED_CLICKHOUSE_VERSIONS = {"25.5.6"}
|
||||
|
||||
|
||||
def pytest_collection_modifyitems(
|
||||
config: pytest.Config, items: list[pytest.Item]
|
||||
) -> None:
|
||||
version = config.getoption("--clickhouse-version")
|
||||
if version in UNSUPPORTED_CLICKHOUSE_VERSIONS:
|
||||
skip = pytest.mark.skip(
|
||||
reason=f"JSON body QB tests require ClickHouse > {version}"
|
||||
)
|
||||
for item in items:
|
||||
item.add_marker(skip)
|
||||
|
||||
|
||||
@pytest.fixture(name="migrator", scope="package")
|
||||
def migrator_json(
|
||||
network: Network,
|
||||
clickhouse: types.TestContainerClickhouse,
|
||||
request: pytest.FixtureRequest,
|
||||
pytestconfig: pytest.Config,
|
||||
) -> types.Operation:
|
||||
"""
|
||||
Package-scoped migrator with ENABLE_LOGS_MIGRATIONS_V2=1.
|
||||
"""
|
||||
return create_migrator(
|
||||
network=network,
|
||||
clickhouse=clickhouse,
|
||||
request=request,
|
||||
pytestconfig=pytestconfig,
|
||||
cache_key="migrator-json-body",
|
||||
env_overrides={
|
||||
"ENABLE_LOGS_MIGRATIONS_V2": "1",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(name="signoz", scope="package")
|
||||
def signoz_json_body(
|
||||
network: Network,
|
||||
zeus: types.TestContainerDocker,
|
||||
gateway: types.TestContainerDocker,
|
||||
sqlstore: types.TestContainerSQL,
|
||||
clickhouse: types.TestContainerClickhouse,
|
||||
request: pytest.FixtureRequest,
|
||||
pytestconfig: pytest.Config,
|
||||
) -> types.SigNoz:
|
||||
"""
|
||||
Package-scoped fixture for SigNoz with BODY_JSON_QUERY_ENABLED=true.
|
||||
"""
|
||||
return create_signoz(
|
||||
network=network,
|
||||
zeus=zeus,
|
||||
gateway=gateway,
|
||||
sqlstore=sqlstore,
|
||||
clickhouse=clickhouse,
|
||||
request=request,
|
||||
pytestconfig=pytestconfig,
|
||||
cache_key="signoz-json-body",
|
||||
env_overrides={
|
||||
"BODY_JSON_QUERY_ENABLED": "true",
|
||||
},
|
||||
)
|
||||
Reference in New Issue
Block a user