Compare commits

...

3 Commits

Author SHA1 Message Date
Abhi Kumar
fd6dca4658 refactor(dashboard): name distribution spec receivers by role 2026-11-12 11:41:33 +05:30
Abhi Kumar
dfb906fe0a chore: regenerate openapi specs 2026-11-12 11:20:45 +05:30
Abhi Kumar
4b6f040e8a feat(dashboard): add distribution panel spec 2026-11-12 11:19:40 +05:30
6 changed files with 597 additions and 56 deletions

View File

@@ -4526,6 +4526,99 @@ components:
required:
- name
type: object
DashboardtypesDistributionAxes:
properties:
x:
allOf:
- $ref: '#/components/schemas/DashboardtypesDistributionXAxis'
description: The bucket axis.
"y":
allOf:
- $ref: '#/components/schemas/DashboardtypesDistributionYAxis'
description: The count axis.
type: object
DashboardtypesDistributionChartAppearance:
properties:
percentiles:
allOf:
- $ref: '#/components/schemas/DashboardtypesDistributionPercentiles'
description: Percentile markers drawn across the bucket axis.
stack:
allOf:
- $ref: '#/components/schemas/DashboardtypesStackMode'
description: 'How groups combine in a bucket: normal (default) stacks them,
none draws them side by side, percent stacks them as shares of the bucket.'
required:
- stack
type: object
DashboardtypesDistributionPanelSpec:
properties:
axes:
allOf:
- $ref: '#/components/schemas/DashboardtypesDistributionAxes'
description: The X (bucket) axis range and Y (count) axis scale.
chartAppearance:
allOf:
- $ref: '#/components/schemas/DashboardtypesDistributionChartAppearance'
description: Stacking and percentile markers.
formatting:
allOf:
- $ref: '#/components/schemas/DashboardtypesPanelFormatting'
description: Unit and decimal precision of the bucket bounds; counts take
no unit.
legend:
allOf:
- $ref: '#/components/schemas/DashboardtypesLegend'
description: Legend placement and series colors.
visualization:
allOf:
- $ref: '#/components/schemas/DashboardtypesBasicVisualization'
description: The time range the bucket counts are summed over.
type: object
DashboardtypesDistributionPercentiles:
properties:
enabled:
description: Whether percentile markers are drawn. Defaults to true.
type: boolean
values:
description: The percentiles to mark, each in (0, 100), at most 5, no duplicates.
Defaults to [50, 90, 99].
items:
format: double
type: number
nullable: true
type: array
required:
- enabled
- values
type: object
DashboardtypesDistributionXAxis:
properties:
max:
description: Buckets wholly above this value are hidden. Null shows them
all.
nullable: true
type: number
min:
description: Buckets wholly below this value are hidden. Null shows them
all.
nullable: true
type: number
type: object
DashboardtypesDistributionYAxis:
properties:
scale:
allOf:
- $ref: '#/components/schemas/DashboardtypesDistributionYScale'
description: 'How counts are spaced: linear (default) or log.'
required:
- scale
type: object
DashboardtypesDistributionYScale:
enum:
- linear
- log
type: string
DashboardtypesDynamicVariableSignal:
enum:
- traces
@@ -5296,6 +5389,7 @@ components:
mapping:
signoz/AreaChartPanel: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesAreaChartPanelSpec'
signoz/BarChartPanel: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesBarChartPanelSpec'
signoz/DistributionPanel: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesDistributionPanelSpec'
signoz/HeatmapPanel: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesHeatmapPanelSpec'
signoz/HistogramPanel: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesHistogramPanelSpec'
signoz/ListPanel: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesListPanelSpec'
@@ -5318,6 +5412,7 @@ components:
- $ref: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTextPanelSpec'
- $ref: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesHeatmapPanelSpec'
- $ref: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesScatterPlotPanelSpec'
- $ref: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesDistributionPanelSpec'
type: object
DashboardtypesPanelPluginKind:
enum:
@@ -5332,6 +5427,7 @@ components:
- signoz/TextPanel
- signoz/HeatmapPanel
- signoz/ScatterPlotPanel
- signoz/DistributionPanel
type: string
DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesAreaChartPanelSpec:
properties:
@@ -5363,6 +5459,21 @@ components:
- kind
- spec
type: object
DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesDistributionPanelSpec:
properties:
kind:
description: The panel kind.
enum:
- signoz/DistributionPanel
type: string
spec:
allOf:
- $ref: '#/components/schemas/DashboardtypesDistributionPanelSpec'
description: The options for this panel kind. {} accepts every default.
required:
- kind
- spec
type: object
DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesHeatmapPanelSpec:
properties:
kind:
@@ -5516,9 +5627,9 @@ components:
- $ref: '#/components/schemas/DashboardtypesPanelPlugin'
description: 'The visualization: kind is one of signoz/TimeSeriesPanel,
signoz/BarChartPanel, signoz/AreaChartPanel, signoz/NumberPanel, signoz/PieChartPanel,
signoz/TablePanel, signoz/HistogramPanel, signoz/ListPanel, signoz/TextPanel
or signoz/HeatmapPanel, and spec holds that kind''s options ({} accepts
every default).'
signoz/TablePanel, signoz/HistogramPanel, signoz/ListPanel, signoz/TextPanel,
signoz/HeatmapPanel or signoz/DistributionPanel, and spec holds that kind''s
options ({} accepts every default).'
queries:
description: The queries the panel runs. Use exactly one entry for a data
panel and [] for signoz/TextPanel. To run several named queries or formulas,
@@ -5654,8 +5765,8 @@ components:
- $ref: '#/components/schemas/Querybuildertypesv5RequestType'
description: 'The result shape the panel requests: time_series for time
series, bar, area and histogram panels, scalar for number, table and pie
panels, raw for list panels, heatmap for heatmap panels, trace for trace
panels.'
panels, raw for list panels, heatmap for heatmap panels, distribution
for distribution panels, trace for trace panels.'
spec:
allOf:
- $ref: '#/components/schemas/DashboardtypesQuerySpec'
@@ -5812,9 +5923,9 @@ components:
(builder query over AI observability traces), signoz/CompositeQuery (several
named queries and formulas), signoz/Formula, signoz/PromQLQuery, signoz/ClickHouseSQL
or signoz/TraceOperator, and spec holds that kind''s query. Pie and table
panels reject signoz/PromQLQuery; heatmap panels reject signoz/AIBuilderQuery
and signoz/TraceOperator; list panels accept only signoz/BuilderQuery
and signoz/AIBuilderQuery.'
panels reject signoz/PromQLQuery; heatmap and distribution panels reject
signoz/AIBuilderQuery and signoz/TraceOperator; list panels accept only
signoz/BuilderQuery and signoz/AIBuilderQuery.'
required:
- plugin
type: object

View File

@@ -669,6 +669,216 @@ func TestValidateHeatmapDashboard(t *testing.T) {
assert.Equal(t, request, rereadRequest)
}
func TestValidateDistributionDashboard(t *testing.T) {
data, err := os.ReadFile("testdata/perses_distribution_panel.json")
require.NoError(t, err, "reading example file")
spec, err := unmarshalDashboard(data)
require.NoError(t, err, "unmarshal and validate failed")
require.IsType(t, &DistributionPanelSpec{}, spec.Panels["p1"].Spec.Plugin.Spec)
panelSpec := spec.Panels["p1"].Spec.Plugin.Spec.(*DistributionPanelSpec)
require.NotNil(t, panelSpec.Axes.X.Min)
assert.Equal(t, 10.0, *panelSpec.Axes.X.Min)
assert.Nil(t, panelSpec.Axes.X.Max)
assert.Equal(t, "log", panelSpec.Axes.Y.Scale.ValueOrDefault())
assert.Equal(t, "none", panelSpec.ChartAppearance.Stack.ValueOrDefault())
assert.True(t, panelSpec.ChartAppearance.Percentiles.Enabled)
assert.Equal(t, []float64{75, 99.9}, panelSpec.ChartAppearance.Percentiles.Values)
dashboard := &DashboardV2{Spec: *spec}
request, err := dashboard.GetPanelQuery(1, 2, "p1")
require.NoError(t, err, "building the panel's query failed")
assert.Equal(t, qb.RequestTypeDistribution, request.RequestType)
require.NoError(t, request.Validate(), "the request built from the panel is not a valid distribution request")
stored, err := json.Marshal(spec)
require.NoError(t, err, "marshal dashboard failed")
reread, err := unmarshalDashboard(stored)
require.NoError(t, err, "the stored dashboard does not validate")
assert.Equal(t, panelSpec, reread.Panels["p1"].Spec.Plugin.Spec)
}
func TestDistributionPanelSpecDefaults(t *testing.T) {
testCases := []struct {
name string
spec string
}{
{name: "EmptySpec", spec: `{}`},
{name: "EmptyChartAppearance", spec: `{"chartAppearance": {}}`},
{name: "EmptyPercentiles", spec: `{"chartAppearance": {"percentiles": {}}}`},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
var plugin PanelPlugin
require.NoError(t, json.Unmarshal([]byte(`{"kind": "signoz/DistributionPanel", "spec": `+testCase.spec+`}`), &plugin))
spec := plugin.Spec.(*DistributionPanelSpec)
assert.Nil(t, spec.Axes.X.Min)
assert.Nil(t, spec.Axes.X.Max)
assert.Equal(t, "linear", spec.Axes.Y.Scale.ValueOrDefault())
assert.Equal(t, "normal", spec.ChartAppearance.Stack.ValueOrDefault())
assert.True(t, spec.ChartAppearance.Percentiles.Enabled)
assert.Equal(t, []float64{50, 90, 99}, spec.ChartAppearance.Percentiles.Values)
stored, err := json.Marshal(spec)
require.NoError(t, err)
assert.Contains(t, string(stored), `"stack":"normal"`)
})
}
}
func TestDistributionPanelSpecPercentileValues(t *testing.T) {
testCases := []struct {
name string
spec string
expectedEnabled bool
expectedValues []float64
}{
{name: "Disabled_KeepsDefaultValues", spec: `{"chartAppearance": {"percentiles": {"enabled": false}}}`, expectedEnabled: false, expectedValues: []float64{50, 90, 99}},
{name: "EmptyValues_MarksNone", spec: `{"chartAppearance": {"percentiles": {"values": []}}}`, expectedEnabled: true, expectedValues: []float64{}},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
var plugin PanelPlugin
require.NoError(t, json.Unmarshal([]byte(`{"kind": "signoz/DistributionPanel", "spec": `+testCase.spec+`}`), &plugin))
spec := plugin.Spec.(*DistributionPanelSpec)
assert.Equal(t, testCase.expectedEnabled, spec.ChartAppearance.Percentiles.Enabled)
assert.Equal(t, testCase.expectedValues, spec.ChartAppearance.Percentiles.Values)
})
}
}
func TestDistributionPanelSpecXMinEqualsMax(t *testing.T) {
var plugin PanelPlugin
require.NoError(t, json.Unmarshal([]byte(`{"kind": "signoz/DistributionPanel", "spec": {"axes": {"x": {"min": 10, "max": 10}}}}`), &plugin))
spec := plugin.Spec.(*DistributionPanelSpec)
require.NotNil(t, spec.Axes.X.Min)
require.NotNil(t, spec.Axes.X.Max)
assert.Equal(t, 10.0, *spec.Axes.X.Min)
assert.Equal(t, 10.0, *spec.Axes.X.Max)
}
func TestDistributionPanelSpecRejections(t *testing.T) {
testCases := []struct {
name string
spec string
expectedMessage string
}{
{
name: "XMin_AboveMax",
spec: `{"axes": {"x": {"min": 20, "max": 10}}}`,
expectedMessage: "axes.x.min 20 is greater than axes.x.max 10",
},
{
name: "YScale_Unknown",
spec: `{"axes": {"y": {"scale": "symlog"}}}`,
expectedMessage: "invalid distribution y scale",
},
{
name: "UnknownField",
spec: `{"axes": {"y": {"softMax": 10}}}`,
expectedMessage: "unknown field \"softMax\"",
},
{
name: "Percentile_Zero",
spec: `{"chartAppearance": {"percentiles": {"values": [0, 50]}}}`,
expectedMessage: "0 is not in (0, 100)",
},
{
name: "Percentile_Hundred",
spec: `{"chartAppearance": {"percentiles": {"values": [100]}}}`,
expectedMessage: "100 is not in (0, 100)",
},
{
name: "Percentile_Duplicate",
spec: `{"chartAppearance": {"percentiles": {"values": [90, 90]}}}`,
expectedMessage: "90 is listed more than once",
},
{
name: "Percentiles_MoreThanFive",
spec: `{"chartAppearance": {"percentiles": {"values": [10, 25, 50, 75, 90, 99]}}}`,
expectedMessage: "at most 5 percentiles",
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
var plugin PanelPlugin
err := json.Unmarshal([]byte(`{"kind": "signoz/DistributionPanel", "spec": `+testCase.spec+`}`), &plugin)
require.Error(t, err)
assert.Contains(t, err.Error(), testCase.expectedMessage)
})
}
}
func TestDistributionPanelQueryKinds(t *testing.T) {
testCases := []struct {
name string
queryPluginKind string
queryPluginSpec string
expectedAllowed bool
}{
{
name: "PromQL_Allowed",
queryPluginKind: "signoz/PromQLQuery",
queryPluginSpec: `{"name": "A", "query": "sum by (le) (increase(signoz_latency_bucket[5m]))"}`,
expectedAllowed: true,
},
{
name: "ClickHouseSQL_Allowed",
queryPluginKind: "signoz/ClickHouseSQL",
queryPluginSpec: `{"name": "A", "query": "SELECT ts, bucket, value FROM cells"}`,
expectedAllowed: true,
},
{
name: "TraceOperator_Refused",
queryPluginKind: "signoz/TraceOperator",
queryPluginSpec: `{"name": "T1", "expression": "A => B"}`,
expectedAllowed: false,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
data := fmt.Sprintf(`{
"variables": [],
"panels": {
"p1": {
"kind": "Panel",
"spec": {
"links": [],
"plugin": {"kind": "signoz/DistributionPanel", "spec": {}},
"queries": [{
"kind": "distribution",
"spec": {
"plugin": {"kind": %q, "spec": %s}
}
}]
}
}
},
"links": [],
"layouts": []
}`, testCase.queryPluginKind, testCase.queryPluginSpec)
_, err := unmarshalDashboard([]byte(data))
if testCase.expectedAllowed {
require.NoError(t, err)
return
}
require.Error(t, err)
assert.Contains(t, err.Error(), "is not supported by panel kind")
})
}
}
func TestInvalidateOneInvalidPanel(t *testing.T) {
data := []byte(`{
"variables": [],

View File

@@ -28,17 +28,18 @@ type PanelPlugin struct {
// (and strips the duplicate parent properties) after reflection.
func (PanelPlugin) PrepareJSONSchema(s *jsonschema.Schema) error {
return markDiscriminator(s, "kind", map[string]string{
string(PanelKindTimeSeries): schemaRef("DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTimeSeriesPanelSpec"),
string(PanelKindBarChart): schemaRef("DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesBarChartPanelSpec"),
string(PanelKindAreaChart): schemaRef("DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesAreaChartPanelSpec"),
string(PanelKindNumber): schemaRef("DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesNumberPanelSpec"),
string(PanelKindPieChart): schemaRef("DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesPieChartPanelSpec"),
string(PanelKindTable): schemaRef("DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTablePanelSpec"),
string(PanelKindHistogram): schemaRef("DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesHistogramPanelSpec"),
string(PanelKindList): schemaRef("DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesListPanelSpec"),
string(PanelKindText): schemaRef("DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTextPanelSpec"),
string(PanelKindHeatmap): schemaRef("DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesHeatmapPanelSpec"),
string(PanelKindScatterPlot): schemaRef("DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesScatterPlotPanelSpec"),
string(PanelKindTimeSeries): schemaRef("DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTimeSeriesPanelSpec"),
string(PanelKindBarChart): schemaRef("DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesBarChartPanelSpec"),
string(PanelKindAreaChart): schemaRef("DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesAreaChartPanelSpec"),
string(PanelKindNumber): schemaRef("DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesNumberPanelSpec"),
string(PanelKindPieChart): schemaRef("DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesPieChartPanelSpec"),
string(PanelKindTable): schemaRef("DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTablePanelSpec"),
string(PanelKindHistogram): schemaRef("DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesHistogramPanelSpec"),
string(PanelKindList): schemaRef("DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesListPanelSpec"),
string(PanelKindText): schemaRef("DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTextPanelSpec"),
string(PanelKindHeatmap): schemaRef("DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesHeatmapPanelSpec"),
string(PanelKindScatterPlot): schemaRef("DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesScatterPlotPanelSpec"),
string(PanelKindDistribution): schemaRef("DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesDistributionPanelSpec"),
})
}
@@ -73,6 +74,7 @@ func (PanelPlugin) JSONSchemaOneOf() []any {
PanelPluginVariant[TextPanelSpec]{Kind: string(PanelKindText)},
PanelPluginVariant[HeatmapPanelSpec]{Kind: string(PanelKindHeatmap)},
PanelPluginVariant[ScatterPlotPanelSpec]{Kind: string(PanelKindScatterPlot)},
PanelPluginVariant[DistributionPanelSpec]{Kind: string(PanelKindDistribution)},
}
}
@@ -236,17 +238,18 @@ func (v VariablePluginVariant[S]) PrepareJSONSchema(s *jsonschema.Schema) error
var (
panelPluginSpecs = map[PanelPluginKind]func() any{
PanelKindTimeSeries: func() any { return new(TimeSeriesPanelSpec) },
PanelKindBarChart: func() any { return new(BarChartPanelSpec) },
PanelKindAreaChart: func() any { return new(AreaChartPanelSpec) },
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) },
PanelKindText: func() any { return new(TextPanelSpec) },
PanelKindHeatmap: func() any { return new(HeatmapPanelSpec) },
PanelKindScatterPlot: func() any { return new(ScatterPlotPanelSpec) },
PanelKindTimeSeries: func() any { return new(TimeSeriesPanelSpec) },
PanelKindBarChart: func() any { return new(BarChartPanelSpec) },
PanelKindAreaChart: func() any { return new(AreaChartPanelSpec) },
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) },
PanelKindText: func() any { return new(TextPanelSpec) },
PanelKindHeatmap: func() any { return new(HeatmapPanelSpec) },
PanelKindScatterPlot: func() any { return new(ScatterPlotPanelSpec) },
PanelKindDistribution: func() any { return new(DistributionPanelSpec) },
}
queryPluginSpecs = map[QueryPluginKind]func() any{
QueryKindBuilder: func() any { return new(BuilderQuerySpec) },
@@ -263,17 +266,18 @@ var (
VariableKindCustom: func() any { return new(CustomVariableSpec) },
}
allowedQueryKinds = map[PanelPluginKind][]QueryPluginKind{
PanelKindTimeSeries: {QueryKindBuilder, QueryKindAIBuilder, QueryKindComposite, QueryKindFormula, QueryKindTraceOperator, QueryKindPromQL, QueryKindClickHouseSQL},
PanelKindBarChart: {QueryKindBuilder, QueryKindAIBuilder, QueryKindComposite, QueryKindFormula, QueryKindTraceOperator, QueryKindPromQL, QueryKindClickHouseSQL},
PanelKindAreaChart: {QueryKindBuilder, QueryKindAIBuilder, QueryKindComposite, QueryKindFormula, QueryKindTraceOperator, QueryKindPromQL, QueryKindClickHouseSQL},
PanelKindNumber: {QueryKindBuilder, QueryKindAIBuilder, QueryKindComposite, QueryKindFormula, QueryKindTraceOperator, QueryKindPromQL, QueryKindClickHouseSQL},
PanelKindHistogram: {QueryKindBuilder, QueryKindAIBuilder, QueryKindComposite, QueryKindFormula, QueryKindTraceOperator, QueryKindPromQL, QueryKindClickHouseSQL},
PanelKindPieChart: {QueryKindBuilder, QueryKindAIBuilder, QueryKindComposite, QueryKindFormula, QueryKindTraceOperator, QueryKindClickHouseSQL},
PanelKindTable: {QueryKindBuilder, QueryKindAIBuilder, QueryKindComposite, QueryKindFormula, QueryKindTraceOperator, QueryKindClickHouseSQL},
PanelKindList: {QueryKindBuilder, QueryKindAIBuilder},
PanelKindText: {},
PanelKindHeatmap: {QueryKindBuilder, QueryKindComposite, QueryKindFormula, QueryKindPromQL, QueryKindClickHouseSQL},
PanelKindScatterPlot: {QueryKindBuilder, QueryKindComposite, QueryKindFormula, QueryKindClickHouseSQL},
PanelKindTimeSeries: {QueryKindBuilder, QueryKindAIBuilder, QueryKindComposite, QueryKindFormula, QueryKindTraceOperator, QueryKindPromQL, QueryKindClickHouseSQL},
PanelKindBarChart: {QueryKindBuilder, QueryKindAIBuilder, QueryKindComposite, QueryKindFormula, QueryKindTraceOperator, QueryKindPromQL, QueryKindClickHouseSQL},
PanelKindAreaChart: {QueryKindBuilder, QueryKindAIBuilder, QueryKindComposite, QueryKindFormula, QueryKindTraceOperator, QueryKindPromQL, QueryKindClickHouseSQL},
PanelKindNumber: {QueryKindBuilder, QueryKindAIBuilder, QueryKindComposite, QueryKindFormula, QueryKindTraceOperator, QueryKindPromQL, QueryKindClickHouseSQL},
PanelKindHistogram: {QueryKindBuilder, QueryKindAIBuilder, QueryKindComposite, QueryKindFormula, QueryKindTraceOperator, QueryKindPromQL, QueryKindClickHouseSQL},
PanelKindPieChart: {QueryKindBuilder, QueryKindAIBuilder, QueryKindComposite, QueryKindFormula, QueryKindTraceOperator, QueryKindClickHouseSQL},
PanelKindTable: {QueryKindBuilder, QueryKindAIBuilder, QueryKindComposite, QueryKindFormula, QueryKindTraceOperator, QueryKindClickHouseSQL},
PanelKindList: {QueryKindBuilder, QueryKindAIBuilder},
PanelKindText: {},
PanelKindHeatmap: {QueryKindBuilder, QueryKindComposite, QueryKindFormula, QueryKindPromQL, QueryKindClickHouseSQL},
PanelKindScatterPlot: {QueryKindBuilder, QueryKindComposite, QueryKindFormula, QueryKindClickHouseSQL},
PanelKindDistribution: {QueryKindBuilder, QueryKindComposite, QueryKindFormula, QueryKindPromQL, QueryKindClickHouseSQL},
}
)

View File

@@ -70,7 +70,7 @@ func (k *PanelKind) UnmarshalJSON(data []byte) error {
type PanelSpec struct {
Display Display `json:"display" required:"true" description:"The panel title and description."`
Plugin PanelPlugin `json:"plugin" required:"true" description:"The visualization: kind is one of signoz/TimeSeriesPanel, signoz/BarChartPanel, signoz/AreaChartPanel, signoz/NumberPanel, signoz/PieChartPanel, signoz/TablePanel, signoz/HistogramPanel, signoz/ListPanel, signoz/TextPanel or signoz/HeatmapPanel, and spec holds that kind's options ({} accepts every default)."`
Plugin PanelPlugin `json:"plugin" required:"true" description:"The visualization: kind is one of signoz/TimeSeriesPanel, signoz/BarChartPanel, signoz/AreaChartPanel, signoz/NumberPanel, signoz/PieChartPanel, signoz/TablePanel, signoz/HistogramPanel, signoz/ListPanel, signoz/TextPanel, signoz/HeatmapPanel or signoz/DistributionPanel, and spec holds that kind's options ({} accepts every default)."`
Queries []Query `json:"queries" required:"true" nullable:"false" description:"The queries the panel runs. Use exactly one entry for a data panel and [] for signoz/TextPanel. To run several named queries or formulas, use one entry whose spec.plugin.kind is signoz/CompositeQuery and put the constituent queries in its plugin spec."`
Links []Link `json:"links,omitzero" description:"Context links offered in the data point drilldown menu of the panel, with dashboard variables resolved in the URL."`
}
@@ -92,13 +92,13 @@ type Link struct {
// ══════════════════════════════════════════════
type Query struct {
Kind qb.RequestType `json:"kind" required:"true" description:"The result shape the panel requests: time_series for time series, bar, area and histogram panels, scalar for number, table and pie panels, raw for list panels, heatmap for heatmap panels, trace for trace panels."`
Kind qb.RequestType `json:"kind" required:"true" description:"The result shape the panel requests: time_series for time series, bar, area and histogram panels, scalar for number, table and pie panels, raw for list panels, heatmap for heatmap panels, distribution for distribution panels, trace for trace panels."`
Spec QuerySpec `json:"spec" required:"true" description:"The query content: an optional name and the query plugin."`
}
type QuerySpec struct {
Name string `json:"name,omitempty" description:"An optional label for the query. The name queries refer to each other by lives in the plugin spec."`
Plugin QueryPlugin `json:"plugin" required:"true" description:"The query: kind is one of signoz/BuilderQuery, signoz/AIBuilderQuery (builder query over AI observability traces), signoz/CompositeQuery (several named queries and formulas), signoz/Formula, signoz/PromQLQuery, signoz/ClickHouseSQL or signoz/TraceOperator, and spec holds that kind's query. Pie and table panels reject signoz/PromQLQuery; heatmap panels reject signoz/AIBuilderQuery and signoz/TraceOperator; list panels accept only signoz/BuilderQuery and signoz/AIBuilderQuery."`
Plugin QueryPlugin `json:"plugin" required:"true" description:"The query: kind is one of signoz/BuilderQuery, signoz/AIBuilderQuery (builder query over AI observability traces), signoz/CompositeQuery (several named queries and formulas), signoz/Formula, signoz/PromQLQuery, signoz/ClickHouseSQL or signoz/TraceOperator, and spec holds that kind's query. Pie and table panels reject signoz/PromQLQuery; heatmap and distribution panels reject signoz/AIBuilderQuery and signoz/TraceOperator; list panels accept only signoz/BuilderQuery and signoz/AIBuilderQuery."`
}
// ══════════════════════════════════════════════

View File

@@ -189,21 +189,22 @@ func (AIBuilderQuerySpec) PrepareJSONSchema(s *jsonschema.Schema) error {
type PanelPluginKind string
const (
PanelKindTimeSeries PanelPluginKind = "signoz/TimeSeriesPanel"
PanelKindBarChart PanelPluginKind = "signoz/BarChartPanel"
PanelKindAreaChart PanelPluginKind = "signoz/AreaChartPanel"
PanelKindNumber PanelPluginKind = "signoz/NumberPanel"
PanelKindPieChart PanelPluginKind = "signoz/PieChartPanel"
PanelKindTable PanelPluginKind = "signoz/TablePanel"
PanelKindHistogram PanelPluginKind = "signoz/HistogramPanel"
PanelKindList PanelPluginKind = "signoz/ListPanel"
PanelKindText PanelPluginKind = "signoz/TextPanel"
PanelKindHeatmap PanelPluginKind = "signoz/HeatmapPanel"
PanelKindScatterPlot PanelPluginKind = "signoz/ScatterPlotPanel"
PanelKindTimeSeries PanelPluginKind = "signoz/TimeSeriesPanel"
PanelKindBarChart PanelPluginKind = "signoz/BarChartPanel"
PanelKindAreaChart PanelPluginKind = "signoz/AreaChartPanel"
PanelKindNumber PanelPluginKind = "signoz/NumberPanel"
PanelKindPieChart PanelPluginKind = "signoz/PieChartPanel"
PanelKindTable PanelPluginKind = "signoz/TablePanel"
PanelKindHistogram PanelPluginKind = "signoz/HistogramPanel"
PanelKindList PanelPluginKind = "signoz/ListPanel"
PanelKindText PanelPluginKind = "signoz/TextPanel"
PanelKindHeatmap PanelPluginKind = "signoz/HeatmapPanel"
PanelKindScatterPlot PanelPluginKind = "signoz/ScatterPlotPanel"
PanelKindDistribution PanelPluginKind = "signoz/DistributionPanel"
)
func (PanelPluginKind) Enum() []any {
return []any{PanelKindTimeSeries, PanelKindBarChart, PanelKindAreaChart, PanelKindNumber, PanelKindPieChart, PanelKindTable, PanelKindHistogram, PanelKindList, PanelKindText, PanelKindHeatmap, PanelKindScatterPlot}
return []any{PanelKindTimeSeries, PanelKindBarChart, PanelKindAreaChart, PanelKindNumber, PanelKindPieChart, PanelKindTable, PanelKindHistogram, PanelKindList, PanelKindText, PanelKindHeatmap, PanelKindScatterPlot, PanelKindDistribution}
}
func (k PanelPluginKind) rendersWithoutQuery() bool {
@@ -346,6 +347,89 @@ func (c HeatmapColors) validate() error {
return nil
}
type DistributionPanelSpec struct {
Visualization BasicVisualization `json:"visualization" description:"The time range the bucket counts are summed over."`
Formatting PanelFormatting `json:"formatting" description:"Unit and decimal precision of the bucket bounds; counts take no unit."`
Axes DistributionAxes `json:"axes" description:"The X (bucket) axis range and Y (count) axis scale."`
Legend Legend `json:"legend" description:"Legend placement and series colors."`
ChartAppearance DistributionChartAppearance `json:"chartAppearance" description:"Stacking and percentile markers."`
}
// UnmarshalJSON decodes over the defaults, since the stack default (normal)
// differs from StackMode's own and percentile markers default to on.
func (spec *DistributionPanelSpec) UnmarshalJSON(data []byte) error {
type alias DistributionPanelSpec
tmp := alias{ChartAppearance: DistributionChartAppearance{
Stack: StackModeNormal,
Percentiles: DistributionPercentiles{Enabled: true, Values: []float64{50, 90, 99}},
}}
dec := json.NewDecoder(bytes.NewReader(data))
dec.DisallowUnknownFields()
if err := dec.Decode(&tmp); err != nil {
return err
}
*spec = DistributionPanelSpec(tmp)
return nil
}
func (spec *DistributionPanelSpec) validate() error {
if err := spec.Axes.X.validate(); err != nil {
return err
}
return spec.ChartAppearance.Percentiles.validate()
}
type DistributionAxes struct {
X DistributionXAxis `json:"x" description:"The bucket axis."`
Y DistributionYAxis `json:"y" description:"The count axis."`
}
// DistributionXAxis draws every bucket equally wide, whatever its bounds.
type DistributionXAxis struct {
Min *float64 `json:"min" description:"Buckets wholly below this value are hidden. Null shows them all."`
Max *float64 `json:"max" description:"Buckets wholly above this value are hidden. Null shows them all."`
}
func (axis DistributionXAxis) validate() error {
if axis.Min != nil && axis.Max != nil && *axis.Min > *axis.Max {
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "axes.x.min %v is greater than axes.x.max %v", *axis.Min, *axis.Max)
}
return nil
}
type DistributionYAxis struct {
Scale DistributionYScale `json:"scale" required:"true" description:"How counts are spaced: linear (default) or log."`
}
type DistributionChartAppearance struct {
Stack StackMode `json:"stack" required:"true" description:"How groups combine in a bucket: normal (default) stacks them, none draws them side by side, percent stacks them as shares of the bucket."`
Percentiles DistributionPercentiles `json:"percentiles" description:"Percentile markers drawn across the bucket axis."`
}
const maxDistributionPercentiles = 5
type DistributionPercentiles struct {
Enabled bool `json:"enabled" required:"true" description:"Whether percentile markers are drawn. Defaults to true."`
Values []float64 `json:"values" required:"true" description:"The percentiles to mark, each in (0, 100), at most 5, no duplicates. Defaults to [50, 90, 99]."`
}
func (percentiles DistributionPercentiles) validate() error {
if len(percentiles.Values) > maxDistributionPercentiles {
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "chartAppearance.percentiles.values: at most %d percentiles, got %d", maxDistributionPercentiles, len(percentiles.Values))
}
seen := make(map[float64]struct{}, len(percentiles.Values))
for _, value := range percentiles.Values {
if value <= 0 || value >= 100 {
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "chartAppearance.percentiles.values: %v is not in (0, 100)", value)
}
if _, ok := seen[value]; ok {
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "chartAppearance.percentiles.values: %v is listed more than once", value)
}
seen[value] = struct{}{}
}
return nil
}
type TextPanelSpec struct {
Mode TextMode `json:"mode" description:"How text is interpreted. Only markdown is supported."`
Text string `json:"text" description:"The content to render."`
@@ -1462,6 +1546,43 @@ func (s *HeatmapYScale) UnmarshalJSON(data []byte) error {
}
}
type DistributionYScale struct{ valuer.String }
var (
DistributionYScaleLinear = DistributionYScale{valuer.NewString("linear")} // default
DistributionYScaleLog = DistributionYScale{valuer.NewString("log")}
)
func (DistributionYScale) Enum() []any {
return []any{DistributionYScaleLinear, DistributionYScaleLog}
}
func (scale DistributionYScale) ValueOrDefault() string {
if scale.IsZero() {
return DistributionYScaleLinear.StringValue()
}
return scale.StringValue()
}
func (scale DistributionYScale) MarshalJSON() ([]byte, error) {
return json.Marshal(scale.ValueOrDefault())
}
func (scale *DistributionYScale) UnmarshalJSON(data []byte) error {
var v string
if err := json.Unmarshal(data, &v); err != nil {
return errors.WrapInvalidInputf(err, ErrCodeDashboardInvalidInput, "invalid distribution y scale: must be a string, one of `linear` or `log`")
}
parsed := DistributionYScale{valuer.NewString(v)}
switch parsed {
case DistributionYScaleLinear, DistributionYScaleLog:
*scale = parsed
return nil
default:
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "invalid distribution y scale %q: must be `linear` or `log`", v)
}
}
type HeatmapColorScale struct{ valuer.String }
var (

View File

@@ -0,0 +1,95 @@
{
"display": {
"name": "latency",
"description": "how request duration is distributed over the range"
},
"variables": [],
"panels": {
"p1": {
"kind": "Panel",
"spec": {
"display": {
"name": "request duration",
"description": ""
},
"plugin": {
"kind": "signoz/DistributionPanel",
"spec": {
"visualization": {
"timePreference": "global_time"
},
"formatting": {
"unit": "s",
"decimalPrecision": "3"
},
"axes": {
"x": {
"min": 10
},
"y": {
"scale": "log"
}
},
"legend": {
"position": "right",
"mode": "list"
},
"chartAppearance": {
"stack": "none",
"percentiles": {
"enabled": true,
"values": [75, 99.9]
}
}
}
},
"links": [],
"queries": [
{
"kind": "distribution",
"spec": {
"plugin": {
"kind": "signoz/BuilderQuery",
"spec": {
"name": "A",
"signal": "metrics",
"aggregations": [
{
"metricName": "http.server.request.duration",
"timeAggregation": "increase",
"spaceAggregation": "sum"
}
],
"groupBy": [
{
"name": "service.name"
}
]
}
}
}
}
]
}
}
},
"links": [],
"layouts": [
{
"kind": "Grid",
"spec": {
"items": [
{
"x": 0,
"y": 0,
"width": 12,
"height": 8,
"content": {
"$ref": "#/spec/panels/p1"
}
}
]
}
}
]
}