Compare commits

..

3 Commits

Author SHA1 Message Date
Gaurav Tewari
98a0d906ed fix(navigation): keep an explicit replace: false in safeNavigate 2026-11-13 01:06:21 +05:30
Ashwin Bhatkal
20e6d44289 fix(api): send array query params as repeated keys (#13457)
Some checks failed
build-staging / staging (push) Has been cancelled
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
cacheci / tests (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
#### Description

- The generated API client now sends array query params as repeated keys
(`states=firing&states=pending`) instead of axios' default `states[]=`.
The backend binds only repeated keys, so a `states` filter sent through
a generated hook was ignored and every rule came back.
- The alert list page and the home page alerts card drop their own
requests that worked around this.
- `states` on `GET /api/v3/rules` is the only array query param in the
generated and AI assistant clients today, so no other request changes.

#### Issues closed by this PR

Closes https://github.com/SigNoz/pulse-pod/issues/719
2026-11-12 16:50:41 +00:00
Ashwin Bhatkal
9fef8eea69 test(alerts): stop the saved views tests timing out on CI (#13481)
#### Description

- `ListAlertRules.views.test.tsx` now gives each test 15 s instead of
Jest's default 5 s. Other slow page tests use the same timeout.
- "deleting the active view falls back to All" is the slowest test in
the file, about 0.8 s locally. CI runs the file about 4x slower (48 to
52 s against 13 s locally), which pushes it past 5 s. It failed the
merge queue for [#13476](https://github.com/SigNoz/signoz/pull/13476)
and [#13301](https://github.com/SigNoz/signoz/pull/13301), and two
unrelated PRs.
- Each step in it finishes well inside the 1 s `waitFor` limit. Only the
total runs over, so this is load, not a stuck wait.

#### Issues closed by this PR

Closes https://github.com/SigNoz/pulse-pod/issues/726
2026-11-12 15:46:37 +00:00
12 changed files with 95 additions and 626 deletions

View File

@@ -4517,99 +4517,6 @@ 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
@@ -5380,7 +5287,6 @@ 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'
@@ -5403,7 +5309,6 @@ components:
- $ref: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTextPanelSpec'
- $ref: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesHeatmapPanelSpec'
- $ref: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesScatterPlotPanelSpec'
- $ref: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesDistributionPanelSpec'
type: object
DashboardtypesPanelPluginKind:
enum:
@@ -5418,7 +5323,6 @@ components:
- signoz/TextPanel
- signoz/HeatmapPanel
- signoz/ScatterPlotPanel
- signoz/DistributionPanel
type: string
DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesAreaChartPanelSpec:
properties:
@@ -5450,21 +5354,6 @@ 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:
@@ -5618,9 +5507,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,
signoz/HeatmapPanel or signoz/DistributionPanel, and spec holds that kind''s
options ({} accepts every default).'
signoz/TablePanel, signoz/HistogramPanel, signoz/ListPanel, signoz/TextPanel
or signoz/HeatmapPanel, 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,
@@ -5756,8 +5645,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, distribution
for distribution panels, trace for trace panels.'
panels, raw for list panels, heatmap for heatmap panels, trace for trace
panels.'
spec:
allOf:
- $ref: '#/components/schemas/DashboardtypesQuerySpec'
@@ -5914,9 +5803,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 and distribution panels reject
signoz/AIBuilderQuery and signoz/TraceOperator; list panels accept only
signoz/BuilderQuery and signoz/AIBuilderQuery.'
panels reject signoz/PromQLQuery; heatmap panels reject signoz/AIBuilderQuery
and signoz/TraceOperator; list panels accept only signoz/BuilderQuery
and signoz/AIBuilderQuery.'
required:
- plugin
type: object

View File

@@ -11,6 +11,8 @@ import axios, { AxiosError, AxiosRequestConfig } from 'axios';
// generated API Instance
const generatedAPIAxiosInstance = axios.create({
baseURL: ENVIRONMENT.baseURL,
// The backend binds repeated keys (`states=a&states=b`); axios' default sends `states[]=`.
paramsSerializer: { indexes: null },
});
let generatedAPIQueryKeyHeaderContext: Record<string, unknown> | undefined;

View File

@@ -1,12 +1,10 @@
import { useMemo } from 'react';
import { useListRulesV3 } from 'api/generated/services/rules';
import {
type ListRulesV3200,
type ListRulesV3Params,
RuletypesAlertStateDTO,
type RuletypesListableRuleDTO,
} from 'api/generated/services/sigNoz.schemas';
import { GeneratedAPIInstance } from 'api/generatedAPIInstance';
const HOME_ALERT_RULES_LIMIT = 5;
@@ -23,18 +21,7 @@ interface HomeAlertRules {
export function useHomeAlertRules(): HomeAlertRules {
const firing = useListRulesV3(FIRING_RULES_PARAMS, {
query: {
cacheTime: 0,
// The backend binds repeated `states=` keys; axios' default sends `states[]=`.
queryFn: ({ signal }) =>
GeneratedAPIInstance<ListRulesV3200>({
url: '/api/v3/rules',
method: 'GET',
params: FIRING_RULES_PARAMS,
paramsSerializer: { indexes: null },
signal,
}),
},
query: { cacheTime: 0 },
});
const firingRules = firing.data?.data.rules;
const latest = useListRulesV3(

View File

@@ -31,6 +31,8 @@ jest.mock('@floating-ui/dom', () => ({
Promise.resolve({ x: 0, y: 0, placement, strategy, middlewareData: {} }),
}));
jest.setTimeout(15_000);
const VIEW_1_ORDER_BY = JSON.stringify({ columnName: 'name', order: 'asc' });
const VIEW_1_QUERY = "severity = 'critical'";

View File

@@ -3,13 +3,11 @@ import logEvent from 'api/common/logEvent';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import { useListRulesV3 } from 'api/generated/services/rules';
import type {
ListRulesV3200,
ListRulesV3Params,
RenderErrorResponseDTO,
RuletypesLabelPairDTO,
} from 'api/generated/services/sigNoz.schemas';
import type { AxiosError } from 'axios';
import { GeneratedAPIInstance } from 'api/generatedAPIInstance';
import { isUndefined } from 'lodash-es';
import type { AlertRule } from './types';
@@ -35,18 +33,7 @@ export function useAlertRulesData(
const hasLoggedEvent = useRef(false);
const rulesResponse = useListRulesV3(params, {
query: {
keepPreviousData: true,
// The backend binds repeated `states=` keys; axios' default sends `states[]=`.
queryFn: ({ signal }) =>
GeneratedAPIInstance<ListRulesV3200>({
url: '/api/v3/rules',
method: 'GET',
params,
paramsSerializer: { indexes: null },
signal,
}),
},
query: { keepPreviousData: true },
});
const rules = useMemo(

View File

@@ -0,0 +1,32 @@
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import { back, getCurrentLocation } from 'lib/router/navigation';
import { act, AllTheProviders, renderHook, waitFor } from 'tests/test-utils';
// jest.config.ts maps `hooks/useSafeNavigate` to a mock; the `.ts` path loads the real hook.
const { useSafeNavigate } = jest.requireActual<
typeof import('hooks/useSafeNavigate')
>('hooks/useSafeNavigate.ts');
describe('useSafeNavigate', () => {
it('adds a history entry for replace: false even when the target adds params', async () => {
const initialSearch = `?${QueryParams.relativeTime}=15m`;
const { result } = renderHook(() => useSafeNavigate(), {
wrapper: ({ children }): JSX.Element => (
<AllTheProviders initialRoute={`${ROUTES.LOGS_EXPLORER}${initialSearch}`}>
{children}
</AllTheProviders>
),
});
act(() => {
result.current.safeNavigate(
`${ROUTES.LOGS_EXPLORER}?${QueryParams.startTime}=1000`,
{ replace: false },
);
});
act(() => back());
await waitFor(() => expect(getCurrentLocation().search).toBe(initialSearch));
});
});

View File

@@ -140,7 +140,7 @@ export const useSafeNavigate = (
const navigationOptions = {
...options,
replace: isDefaultParamsNavigation || options?.replace,
replace: options?.replace ?? isDefaultParamsNavigation,
};
if (typeof to === 'string') {

View File

@@ -669,216 +669,6 @@ 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,18 +28,17 @@ 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(PanelKindDistribution): schemaRef("DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesDistributionPanelSpec"),
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"),
})
}
@@ -74,7 +73,6 @@ func (PanelPlugin) JSONSchemaOneOf() []any {
PanelPluginVariant[TextPanelSpec]{Kind: string(PanelKindText)},
PanelPluginVariant[HeatmapPanelSpec]{Kind: string(PanelKindHeatmap)},
PanelPluginVariant[ScatterPlotPanelSpec]{Kind: string(PanelKindScatterPlot)},
PanelPluginVariant[DistributionPanelSpec]{Kind: string(PanelKindDistribution)},
}
}
@@ -238,18 +236,17 @@ 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) },
PanelKindDistribution: func() any { return new(DistributionPanelSpec) },
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) },
}
queryPluginSpecs = map[QueryPluginKind]func() any{
QueryKindBuilder: func() any { return new(BuilderQuerySpec) },
@@ -266,18 +263,17 @@ 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},
PanelKindDistribution: {QueryKindBuilder, QueryKindComposite, QueryKindFormula, QueryKindPromQL, 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},
}
)

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, signoz/HeatmapPanel or signoz/DistributionPanel, 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 or signoz/HeatmapPanel, 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, distribution for distribution 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, 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 and distribution 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 panels reject signoz/AIBuilderQuery and signoz/TraceOperator; list panels accept only signoz/BuilderQuery and signoz/AIBuilderQuery."`
}
// ══════════════════════════════════════════════

View File

@@ -189,22 +189,21 @@ 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"
PanelKindDistribution PanelPluginKind = "signoz/DistributionPanel"
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"
)
func (PanelPluginKind) Enum() []any {
return []any{PanelKindTimeSeries, PanelKindBarChart, PanelKindAreaChart, PanelKindNumber, PanelKindPieChart, PanelKindTable, PanelKindHistogram, PanelKindList, PanelKindText, PanelKindHeatmap, PanelKindScatterPlot, PanelKindDistribution}
return []any{PanelKindTimeSeries, PanelKindBarChart, PanelKindAreaChart, PanelKindNumber, PanelKindPieChart, PanelKindTable, PanelKindHistogram, PanelKindList, PanelKindText, PanelKindHeatmap, PanelKindScatterPlot}
}
func (k PanelPluginKind) rendersWithoutQuery() bool {
@@ -347,89 +346,6 @@ 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."`
@@ -1546,43 +1462,6 @@ 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

@@ -1,95 +0,0 @@
{
"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"
}
}
]
}
}
]
}