Compare commits

..

9 Commits

Author SHA1 Message Date
swapnil-signoz
daafdce32a Merge branch 'issue-2946' of https://github.com/SigNoz/signoz into issue-2946 2026-09-18 19:28:41 +05:30
swapnil-signoz
754068369c refactor: folding cloud integration services into account 2026-09-18 19:01:38 +05:30
Swapnil Nakade
83402ebea8 Merge branch 'main' into issue-2946 2026-09-17 23:34:06 +05:30
swapnil-signoz
1285e0a5f2 chore: generating openapi specs 2026-09-17 23:33:38 +05:30
swapnil-signoz
f82a9ee528 feat: enable FGA for cloud integration 2026-09-17 23:09:53 +05:30
Nikhil Soni
9f03fea0f3 fix(querybuilder): prefer resource over any context for ambiguous filter keys (#12888)
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

- A logs filter on a bare key that lives in **both** resource and
another context (body or scope) ANDed the two: the resource candidate
built the `__resource_filter` fingerprint CTE while the other candidate
landed as a required main-query term, so the query matched almost
nothing.
- `ResolveLogicalFields` only preferred resource over `attribute`.
Generalized it to prefer resource over **any** other context (attribute,
body, scope, …); other contexts stay reachable via their qualified names
(e.g. `body.service.name`).

#### Issues closed by this PR

Closes SigNoz/engineering-pod#6086
Part of https://github.com/SigNoz/platform-pod/issues/3158

#### Additional Information

Generalized rather than special-casing body/scope, since any future
context would hit the same fingerprint-CTE trap.
2026-09-17 14:45:45 +00:00
Naman Verma
d1a382945c fix: read Bearer/bearer/BEARER properly in v2 for webhook notification channels (#12890)
<!--A few plain bullets saying what changed and why, for a reviewer
skimming it - not a wall of text, not a restatement of the diff, not
generated boilerplate.-->
#### Description

Webhook notification channels with a bearer token authorisation work in
v1 with all three spellings `Bearer/bearer/BEARER`, but v2 API was not
accepting anything other than `Bearer`. This PR changes the conversion
from receiver -> gettable flow.

Also, error messages are made better in 2 places.
2026-09-17 11:57:11 +00:00
Nikhil Soni
59af5e0367 refactor(telemetrystore): drop app-side bulk-filtering override (#12871)
#### Description

- Stop managing `secondary_indices_enable_bulk_filtering` from the app.
It was hardcoded to `false` in the query hook as a workaround for
[ClickHouse#82283](https://github.com/ClickHouse/ClickHouse/issues/82283)
(`CANNOT_READ_ALL_DATA` with SET-type skip indexes).
- That bug is fixed
([ClickHouse#87817](https://github.com/ClickHouse/ClickHouse/pull/87817),
backported to 25.7/25.8/25.9) and prod runs 25.12. Verified on a local
25.12.5 container against `signoz_index_v3` that the crash no longer
reproduces with bulk filtering enabled.
- Removes the hook override plus the now-unused config field and
`example.yaml` entry. The setting reverts to being controlled
server-side via ClickHouse profiles (the charts change tracked in the
same issue).

#### Issues closed by this PR

Closes SigNoz/engineering-pod#5915

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-09-17 09:40:26 +00:00
Pandey
8286e787b2 fix(tracefunnel): quote step names in slow and error trace queries (#12886)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
cacheci / tests (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
#### Description

- `#12593` moved the n-step trace-funnel query builders onto
`clickhousesql.StringLiteral`, but the two-step `slow-traces` and
`error-traces` builders still interpolated `service_name`/`span_name`
into the SQL string literal raw.
- Route those four values through the same helper, so every funnel query
builder quotes step names consistently.

#### Additional Information

- No behaviour change for ordinary names; the
`slow-traces`/`error-traces` funnel queries now handle names containing
a quote the same way the rest of the module already does.
2026-09-17 07:01:21 +00:00
80 changed files with 917 additions and 1287 deletions

View File

@@ -202,7 +202,6 @@ telemetrystore:
max_bytes_to_read: 0
max_result_rows: 0
ignore_data_skipping_indices: ""
secondary_indices_enable_bulk_filtering: false
##################### Prometheus #####################
prometheus:

View File

@@ -3210,69 +3210,6 @@ components:
repeatVariable:
type: string
type: object
DashboardtypesAIBuilderQuerySpec:
properties:
aggregations:
items:
$ref: '#/components/schemas/Querybuildertypesv5TraceAggregation'
nullable: true
type: array
bucketOptions:
$ref: '#/components/schemas/Querybuildertypesv5BucketOptions'
cursor:
type: string
disabled:
type: boolean
filter:
$ref: '#/components/schemas/Querybuildertypesv5Filter'
functions:
items:
$ref: '#/components/schemas/Querybuildertypesv5Function'
nullable: true
type: array
groupBy:
items:
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
nullable: true
type: array
having:
$ref: '#/components/schemas/Querybuildertypesv5Having'
legend:
type: string
limit:
type: integer
limitBy:
$ref: '#/components/schemas/Querybuildertypesv5LimitBy'
name:
type: string
offset:
type: integer
order:
items:
$ref: '#/components/schemas/Querybuildertypesv5OrderBy'
nullable: true
type: array
secondaryAggregations:
items:
$ref: '#/components/schemas/Querybuildertypesv5SecondaryAggregation'
nullable: true
type: array
selectFields:
items:
$ref: '#/components/schemas/TelemetrytypesTelemetryFieldKey'
nullable: true
type: array
signal:
enum:
- traces
type: string
source:
$ref: '#/components/schemas/TelemetrytypesSource'
stepInterval:
$ref: '#/components/schemas/Querybuildertypesv5Step'
required:
- signal
type: object
DashboardtypesAxes:
properties:
isLogScale:
@@ -4196,7 +4133,6 @@ components:
DashboardtypesQueryPlugin:
discriminator:
mapping:
signoz/AIBuilderQuery: '#/components/schemas/DashboardtypesQueryPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesAIBuilderQuerySpec'
signoz/BuilderQuery: '#/components/schemas/DashboardtypesQueryPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesBuilderQuerySpec'
signoz/ClickHouseSQL: '#/components/schemas/DashboardtypesQueryPluginVariantGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5ClickHouseQuery'
signoz/CompositeQuery: '#/components/schemas/DashboardtypesQueryPluginVariantGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5CompositeQuery'
@@ -4206,7 +4142,6 @@ components:
propertyName: kind
oneOf:
- $ref: '#/components/schemas/DashboardtypesQueryPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesBuilderQuerySpec'
- $ref: '#/components/schemas/DashboardtypesQueryPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesAIBuilderQuerySpec'
- $ref: '#/components/schemas/DashboardtypesQueryPluginVariantGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5CompositeQuery'
- $ref: '#/components/schemas/DashboardtypesQueryPluginVariantGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5QueryBuilderFormula'
- $ref: '#/components/schemas/DashboardtypesQueryPluginVariantGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5PromQuery'
@@ -4216,25 +4151,12 @@ components:
DashboardtypesQueryPluginKind:
enum:
- signoz/BuilderQuery
- signoz/AIBuilderQuery
- signoz/CompositeQuery
- signoz/Formula
- signoz/PromQLQuery
- signoz/ClickHouseSQL
- signoz/TraceOperator
type: string
DashboardtypesQueryPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesAIBuilderQuerySpec:
properties:
kind:
enum:
- signoz/AIBuilderQuery
type: string
spec:
$ref: '#/components/schemas/DashboardtypesAIBuilderQuerySpec'
required:
- kind
- spec
type: object
DashboardtypesQueryPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesBuilderQuerySpec:
properties:
kind:
@@ -11038,9 +10960,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- VIEWER
- cloud-integration:read
- tokenizer:
- VIEWER
- cloud-integration:read
summary: Agent check-in
tags:
- cloudintegration
@@ -11090,9 +11012,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- ADMIN
- cloud-integration:list
- tokenizer:
- ADMIN
- cloud-integration:list
summary: List accounts
tags:
- cloudintegration
@@ -11147,9 +11069,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- ADMIN
- cloud-integration:create
- tokenizer:
- ADMIN
- cloud-integration:create
summary: Create account
tags:
- cloudintegration
@@ -11192,9 +11114,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- ADMIN
- cloud-integration:delete
- tokenizer:
- ADMIN
- cloud-integration:delete
summary: Disconnect account
tags:
- cloudintegration
@@ -11260,9 +11182,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- ADMIN
- cloud-integration:read
- tokenizer:
- ADMIN
- cloud-integration:read
summary: Get account
tags:
- cloudintegration
@@ -11309,9 +11231,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- ADMIN
- cloud-integration:update
- tokenizer:
- ADMIN
- cloud-integration:update
summary: Update account
tags:
- cloudintegration
@@ -11367,9 +11289,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- ADMIN
- cloud-integration:list
- tokenizer:
- ADMIN
- cloud-integration:list
summary: List account services metadata
tags:
- cloudintegration
@@ -11442,9 +11364,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- ADMIN
- cloud-integration:read
- tokenizer:
- ADMIN
- cloud-integration:read
summary: Get service for account
tags:
- cloudintegration
@@ -11496,9 +11418,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- ADMIN
- cloud-integration:update
- tokenizer:
- ADMIN
- cloud-integration:update
summary: Update service
tags:
- cloudintegration
@@ -11553,9 +11475,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- VIEWER
- cloud-integration:read
- tokenizer:
- VIEWER
- cloud-integration:read
summary: Agent check-in
tags:
- cloudintegration
@@ -11606,9 +11528,17 @@ paths:
description: Internal Server Error
security:
- api_key:
- ADMIN
- ingestion-key:create
- serviceaccount:create
- factor-api-key:create
- serviceaccount:attach
- role:attach
- tokenizer:
- ADMIN
- ingestion-key:create
- serviceaccount:create
- factor-api-key:create
- serviceaccount:attach
- role:attach
summary: Get connection credentials
tags:
- cloudintegration
@@ -11658,10 +11588,8 @@ paths:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- ADMIN
- tokenizer:
- ADMIN
- api_key: []
- tokenizer: []
summary: List services metadata
tags:
- cloudintegration
@@ -11716,10 +11644,8 @@ paths:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- ADMIN
- tokenizer:
- ADMIN
- api_key: []
- tokenizer: []
summary: Get service
tags:
- cloudintegration

View File

@@ -4009,71 +4009,6 @@ export interface DashboardGridLayoutSpecDTO {
repeatVariable?: string;
}
export enum DashboardtypesAIBuilderQuerySpecDTOSignal {
traces = 'traces',
}
export interface DashboardtypesAIBuilderQuerySpecDTO {
/**
* @type array,null
*/
aggregations?: Querybuildertypesv5TraceAggregationDTO[] | null;
bucketOptions?: Querybuildertypesv5BucketOptionsDTO;
/**
* @type string
*/
cursor?: string;
/**
* @type boolean
*/
disabled?: boolean;
filter?: Querybuildertypesv5FilterDTO;
/**
* @type array,null
*/
functions?: Querybuildertypesv5FunctionDTO[] | null;
/**
* @type array,null
*/
groupBy?: Querybuildertypesv5GroupByKeyDTO[] | null;
having?: Querybuildertypesv5HavingDTO;
/**
* @type string
*/
legend?: string;
/**
* @type integer
*/
limit?: number;
limitBy?: Querybuildertypesv5LimitByDTO;
/**
* @type string
*/
name?: string;
/**
* @type integer
*/
offset?: number;
/**
* @type array,null
*/
order?: Querybuildertypesv5OrderByDTO[] | null;
/**
* @type array,null
*/
secondaryAggregations?: Querybuildertypesv5SecondaryAggregationDTO[] | null;
/**
* @type array,null
*/
selectFields?: TelemetrytypesTelemetryFieldKeyDTO[] | null;
/**
* @enum traces
* @type string
*/
signal: DashboardtypesAIBuilderQuerySpecDTOSignal;
source?: TelemetrytypesSourceDTO;
stepInterval?: Querybuildertypesv5StepDTO;
}
export interface DashboardtypesAxesDTO {
/**
* @type boolean
@@ -5167,18 +5102,6 @@ export interface DashboardtypesQueryPluginVariantGithubComSigNozSignozPkgTypesDa
spec: DashboardtypesBuilderQuerySpecDTO;
}
export enum DashboardtypesQueryPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesAIBuilderQuerySpecDTOKind {
'signoz/AIBuilderQuery' = 'signoz/AIBuilderQuery',
}
export interface DashboardtypesQueryPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesAIBuilderQuerySpecDTO {
/**
* @enum signoz/AIBuilderQuery
* @type string
*/
kind: DashboardtypesQueryPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesAIBuilderQuerySpecDTOKind;
spec: DashboardtypesAIBuilderQuerySpecDTO;
}
export enum DashboardtypesQueryPluginVariantGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5CompositeQueryDTOKind {
'signoz/CompositeQuery' = 'signoz/CompositeQuery',
}
@@ -5470,7 +5393,6 @@ export interface DashboardtypesQueryPluginVariantGithubComSigNozSignozPkgTypesQu
export type DashboardtypesQueryPluginDTO =
| DashboardtypesQueryPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesBuilderQuerySpecDTO
| DashboardtypesQueryPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesAIBuilderQuerySpecDTO
| DashboardtypesQueryPluginVariantGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5CompositeQueryDTO
| DashboardtypesQueryPluginVariantGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5QueryBuilderFormulaDTO
| DashboardtypesQueryPluginVariantGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5PromQueryDTO
@@ -6147,7 +6069,6 @@ export interface DashboardtypesPostablePublicDashboardDTO {
export enum DashboardtypesQueryPluginKindDTO {
'signoz/BuilderQuery' = 'signoz/BuilderQuery',
'signoz/AIBuilderQuery' = 'signoz/AIBuilderQuery',
'signoz/CompositeQuery' = 'signoz/CompositeQuery',
'signoz/Formula' = 'signoz/Formula',
'signoz/PromQLQuery' = 'signoz/PromQLQuery',

View File

@@ -30,10 +30,7 @@ const mapQueryFromV5 = (compositeQuery: ICompositeMetricQuery): Query => {
> = {};
const builderQueryTypes: Record<
string,
| 'builder_query'
| 'builder_ai_query'
| 'builder_formula'
| 'builder_trace_operator'
'builder_query' | 'builder_formula' | 'builder_trace_operator'
> = {};
const promQueries: IPromQLQuery[] = [];
const clickhouseQueries: IClickHouseQuery[] = [];
@@ -47,14 +44,6 @@ const mapQueryFromV5 = (compositeQuery: ICompositeMetricQuery): Query => {
);
builderQueryTypes[spec.name] = 'builder_query';
}
} else if (q.type === 'builder_ai_query') {
if (spec.name) {
builderQueries[spec.name] = {
...convertBuilderQueryToIBuilderQuery(spec as BuilderQuery),
builderQueryType: 'builder_ai_query',
};
builderQueryTypes[spec.name] = 'builder_ai_query';
}
} else if (q.type === 'builder_formula') {
if (spec.name) {
builderQueries[spec.name] = convertQueryBuilderFormulaToIBuilderFormula(

View File

@@ -15,10 +15,7 @@ export const transformQueryBuilderDataModel = (
data: BuilderQueryDataResourse,
queryTypes?: Record<
string,
| 'builder_query'
| 'builder_ai_query'
| 'builder_formula'
| 'builder_trace_operator'
'builder_query' | 'builder_formula' | 'builder_trace_operator'
>,
): QueryBuilderData => {
const queryData: QueryBuilderData['queryData'] = [];

View File

@@ -1,7 +1,3 @@
import {
isBuilderEnvelope,
isBuilderPluginKind,
} from '../../../queryV5/builderEnvelope';
import type {
DashboardtypesDashboardSpecDTOPanels,
DashboardtypesQueryDTO,
@@ -24,13 +20,15 @@ function forEachBuilderSpec(
}
if (plugin.kind === 'signoz/CompositeQuery') {
const composite = plugin.spec as Querybuildertypesv5CompositeQueryDTO;
(composite.queries ?? []).filter(isBuilderEnvelope).forEach((envelope) => {
const { spec } = envelope as Querybuildertypesv5QueryEnvelopeBuilderDTO;
if (spec) {
fn(spec as Querybuildertypesv5BuilderQuerySpecDTO);
}
});
} else if (isBuilderPluginKind(plugin.kind)) {
(composite.queries ?? [])
.filter((envelope) => envelope.type === 'builder_query')
.forEach((envelope) => {
const { spec } = envelope as Querybuildertypesv5QueryEnvelopeBuilderDTO;
if (spec) {
fn(spec as Querybuildertypesv5BuilderQuerySpecDTO);
}
});
} else if (plugin.kind === 'signoz/BuilderQuery') {
fn(plugin.spec as Querybuildertypesv5BuilderQuerySpecDTO);
}
}

View File

@@ -11,7 +11,6 @@ import {
textContainsVariableReference,
} from 'lib/dashboardVariables/variableReference';
import { isBuilderEnvelope } from '../../../queryV5/builderEnvelope';
import { toQueryEnvelopes } from '../../../queryV5/buildQueryRangeRequest';
import { getTextPanelBody } from './getTextPanelBody';
import { dtoToFormModel } from '../variableAdapters';
@@ -55,7 +54,7 @@ function envelopeReferenceText(
const spec = envelope.spec as
| { query?: string; filter?: { expression?: string } }
| undefined;
if (isBuilderEnvelope(envelope)) {
if (envelope.type === 'builder_query') {
const text = spec?.filter?.expression;
return typeof text === 'string' ? { kind: 'builder', text } : null;
}
@@ -233,7 +232,7 @@ export function findApplyUsages(
});
};
if (isBuilderEnvelope(envelope)) {
if (envelope.type === 'builder_query') {
const spec = envelope.spec as
| { filter?: { expression?: string } }
| undefined;
@@ -295,7 +294,7 @@ export function isVariableAppliedToAllPanels(
return true;
}
return toQueryEnvelopes(queries).every((envelope) => {
if (isBuilderEnvelope(envelope)) {
if (envelope.type === 'builder_query') {
const spec = envelope.spec as
| { filter?: { expression?: string } }
| undefined;

View File

@@ -7,8 +7,8 @@ import type {
import { getPanelDefinition } from 'pages/DashboardPage/DashboardContainer/Panels/registry';
import { SectionKind } from 'pages/DashboardPage/DashboardContainer/Panels/types/sections';
import { getSupportedSignals } from 'pages/DashboardPage/DashboardContainer/Panels/capabilities';
import type { PanelQueryMode } from 'pages/DashboardPage/DashboardContainer/Panels/types/queryModes';
import { resolveSignal } from 'pages/DashboardPage/DashboardContainer/Panels/utils/getBuilderQueries';
import type { EQueryType } from 'types/common/dashboard';
import type { LegendSeries } from 'pages/DashboardPage/DashboardContainer/Panels/utils/legendSeries';
import type { TableColumnOption } from '../hooks/useTableColumns';
@@ -30,7 +30,7 @@ interface ConfigPaneProps {
* panel types the visualization switcher disables — read from the provider, not the
* spec, because a new panel's spec has no query until staged.
*/
mode: PanelQueryMode;
queryType: EQueryType;
/** Panel's resolved series, provided to sections that need them (legend colors). */
legendSeries: LegendSeries[];
/** Table panel's resolved value columns, for the table-only editors. */
@@ -57,7 +57,7 @@ function ConfigPane({
spec,
onChangeSpec,
onChangePanelKind,
mode,
queryType,
legendSeries,
tableColumns,
stepInterval,
@@ -125,7 +125,7 @@ function ConfigPane({
signal={signal}
panelKind={panelKind}
onChangePanelKind={onChangePanelKind}
mode={mode}
queryType={queryType}
stepInterval={stepInterval}
metricUnit={metricUnit}
/>
@@ -149,7 +149,7 @@ function ConfigPane({
signal={signal}
panelKind={panelKind}
onChangePanelKind={onChangePanelKind}
mode={mode}
queryType={queryType}
stepInterval={stepInterval}
metricUnit={metricUnit}
/>

View File

@@ -1,8 +1,8 @@
import { Typography } from '@signozhq/ui/typography';
import type { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
import type { EQueryType } from 'types/common/dashboard';
import type { PanelKind } from '../../../Panels/types/panelKind';
import type { PanelQueryMode } from '../../../Panels/types/queryModes';
import ConfigSelect from '../controls/ConfigSelect/ConfigSelect';
import styles from './PanelTypeSwitcher.module.scss';
@@ -11,8 +11,8 @@ import { usePanelTypeSelectItems } from './usePanelTypeSelectItems';
interface PanelTypeSwitcherProps {
/** The current panel kind (selected value). */
panelKind: PanelKind;
/** Active query mode — a kind that can't be authored in it is disabled (e.g. List is Query-Builder-only, so PromQL/ClickHouse/AI disable it). */
mode: PanelQueryMode;
/** Active query type — a kind that can't be authored in it is disabled (e.g. List is Query-Builder-only, so PromQL/ClickHouse disable it). */
queryType: EQueryType;
/** Panel's current signal — also gates the disabled rule (List needs logs/traces, not metrics). */
signal?: TelemetrytypesSignalDTO;
onChange: (kind: PanelKind) => void;
@@ -26,11 +26,11 @@ interface PanelTypeSwitcherProps {
*/
function PanelTypeSwitcher({
panelKind,
mode,
queryType,
signal,
onChange,
}: PanelTypeSwitcherProps): JSX.Element {
const items = usePanelTypeSelectItems({ mode, signal });
const items = usePanelTypeSelectItems({ queryType, signal });
return (
<div className={styles.field}>

View File

@@ -43,28 +43,18 @@ describe('PanelTypeSwitcher', () => {
jest.clearAllMocks();
// List supports only logs/traces; every other kind also supports metrics.
// Query-type support comes from SUPPORTED_QUERY_TYPES (all three by default).
mockGetPanelDefinition.mockImplementation((kind: string) => {
const signals =
mockGetPanelDefinition.mockImplementation((kind: string) => ({
mode: 'query',
supportedSignals:
kind === 'signoz/ListPanel'
? ['logs', 'traces']
: ['metrics', 'logs', 'traces'];
const queryTypes = SUPPORTED_QUERY_TYPES[kind] ?? [
: ['metrics', 'logs', 'traces'],
supportedQueryTypes: SUPPORTED_QUERY_TYPES[kind] ?? [
EQueryType.QUERY_BUILDER,
EQueryType.CLICKHOUSE,
EQueryType.PROM,
];
return {
mode: 'query',
supportedQueryModes: Object.fromEntries(
queryTypes.map((queryType: EQueryType) => [
queryType,
queryType === EQueryType.QUERY_BUILDER
? { kind: 'signal', signals }
: { kind: 'signal-less' },
]),
),
};
});
],
}));
});
it('fires onChange with the chosen plugin kind', () => {
@@ -72,7 +62,7 @@ describe('PanelTypeSwitcher', () => {
render(
<PanelTypeSwitcher
panelKind="signoz/TimeSeriesPanel"
mode={EQueryType.QUERY_BUILDER}
queryType={EQueryType.QUERY_BUILDER}
onChange={onChange}
/>,
);
@@ -87,7 +77,7 @@ describe('PanelTypeSwitcher', () => {
render(
<PanelTypeSwitcher
panelKind="signoz/TimeSeriesPanel"
mode={EQueryType.QUERY_BUILDER}
queryType={EQueryType.QUERY_BUILDER}
signal={TelemetrytypesSignalDTO.metrics}
onChange={jest.fn()}
/>,
@@ -103,7 +93,7 @@ describe('PanelTypeSwitcher', () => {
render(
<PanelTypeSwitcher
panelKind="signoz/TimeSeriesPanel"
mode={EQueryType.QUERY_BUILDER}
queryType={EQueryType.QUERY_BUILDER}
onChange={jest.fn()}
/>,
);
@@ -118,7 +108,7 @@ describe('PanelTypeSwitcher', () => {
render(
<PanelTypeSwitcher
panelKind="signoz/TimeSeriesPanel"
mode={EQueryType.PROM}
queryType={EQueryType.PROM}
onChange={jest.fn()}
/>,
);
@@ -135,7 +125,7 @@ describe('PanelTypeSwitcher', () => {
render(
<PanelTypeSwitcher
panelKind="signoz/TablePanel"
mode={EQueryType.CLICKHOUSE}
queryType={EQueryType.CLICKHOUSE}
onChange={jest.fn()}
/>,
);

View File

@@ -11,14 +11,14 @@ describe('getPanelTypeDisabledReason', () => {
expect(
getPanelTypeDisabledReason({
kind: 'signoz/TimeSeriesPanel',
mode: PROM,
queryType: PROM,
label: 'Time Series',
}),
).toBeUndefined();
expect(
getPanelTypeDisabledReason({
kind: 'signoz/ListPanel',
mode: QUERY_BUILDER,
queryType: QUERY_BUILDER,
signal: logs,
label: 'List',
}),
@@ -29,21 +29,21 @@ describe('getPanelTypeDisabledReason', () => {
expect(
getPanelTypeDisabledReason({
kind: 'signoz/ListPanel',
mode: PROM,
queryType: PROM,
label: 'List',
}),
).toBe("List isn't available for PromQL queries");
expect(
getPanelTypeDisabledReason({
kind: 'signoz/ListPanel',
mode: CLICKHOUSE,
queryType: CLICKHOUSE,
label: 'List',
}),
).toBe("List isn't available for ClickHouse queries");
expect(
getPanelTypeDisabledReason({
kind: 'signoz/TablePanel',
mode: PROM,
queryType: PROM,
label: 'Table',
}),
).toBe("Table isn't available for PromQL queries");
@@ -53,7 +53,7 @@ describe('getPanelTypeDisabledReason', () => {
expect(
getPanelTypeDisabledReason({
kind: 'signoz/ListPanel',
mode: QUERY_BUILDER,
queryType: QUERY_BUILDER,
signal: metrics,
label: 'List',
}),
@@ -64,7 +64,7 @@ describe('getPanelTypeDisabledReason', () => {
expect(
getPanelTypeDisabledReason({
kind: 'signoz/ListPanel',
mode: PROM,
queryType: PROM,
signal: metrics,
label: 'List',
}),

View File

@@ -1,16 +1,16 @@
import { useMemo } from 'react';
import type { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
import type { EQueryType } from 'types/common/dashboard';
import { PANEL_OPTIONS } from '../../../Panels/registry';
import type { PanelKind } from '../../../Panels/types/panelKind';
import type { PanelQueryMode } from '../../../Panels/types/queryModes';
import type { ConfigSelectItem } from '../controls/ConfigSelect/ConfigSelect';
import { getPanelTypeDisabledReason } from './utils';
interface UsePanelTypeSelectItemsArgs {
/** Active query mode — a kind that can't be authored in it is disabled. */
mode: PanelQueryMode;
/** Active query type — a kind that can't be authored in it is disabled. */
queryType: EQueryType;
/** Current datasource — also gates the disabled rule (List needs logs/traces, not metrics). */
signal?: TelemetrytypesSignalDTO;
}
@@ -22,7 +22,7 @@ interface UsePanelTypeSelectItemsArgs {
* modal's header so the two selectors apply the same rule and can't drift.
*/
export function usePanelTypeSelectItems({
mode,
queryType,
signal,
}: UsePanelTypeSelectItemsArgs): ConfigSelectItem<PanelKind>[] {
return useMemo(
@@ -31,7 +31,7 @@ export function usePanelTypeSelectItems({
// One reason drives both the disabled flag and the tooltip, so they can't disagree.
const disabledReason = getPanelTypeDisabledReason({
kind,
mode,
queryType,
signal,
label: displayName,
});
@@ -43,6 +43,6 @@ export function usePanelTypeSelectItems({
tooltip: disabledReason,
};
}),
[mode, signal],
[queryType, signal],
);
}

View File

@@ -3,14 +3,10 @@ import { EQueryType } from 'types/common/dashboard';
import {
isStaticPanelKind,
isQueryModeSupportedByPanelKind,
isQueryTypeSupportedByPanelKind,
isSignalSupported,
} from '../../../Panels/capabilities';
import type { PanelKind } from '../../../Panels/types/panelKind';
import {
AI_QUERY_MODE,
type PanelQueryMode,
} from '../../../Panels/types/queryModes';
const QUERY_TYPE_LABEL: Record<EQueryType, string> = {
[EQueryType.QUERY_BUILDER]: 'Query Builder',
@@ -18,11 +14,6 @@ const QUERY_TYPE_LABEL: Record<EQueryType, string> = {
[EQueryType.PROM]: 'PromQL',
};
const MODE_LABEL: Record<PanelQueryMode, string> = {
...QUERY_TYPE_LABEL,
[AI_QUERY_MODE]: 'AI Query Builder',
};
const SIGNAL_LABEL: Record<TelemetrytypesSignalDTO, string> = {
[TelemetrytypesSignalDTO.logs]: 'logs',
[TelemetrytypesSignalDTO.traces]: 'traces',
@@ -38,12 +29,12 @@ const SIGNAL_LABEL: Record<TelemetrytypesSignalDTO, string> = {
*/
export function getPanelTypeDisabledReason({
kind,
mode,
queryType,
signal,
label,
}: {
kind: PanelKind;
mode: PanelQueryMode;
queryType: EQueryType;
signal?: TelemetrytypesSignalDTO;
label: string;
}): string | undefined {
@@ -53,10 +44,10 @@ export function getPanelTypeDisabledReason({
if (isStaticPanelKind(kind)) {
return undefined;
}
if (!isQueryModeSupportedByPanelKind(kind, mode)) {
return `${label} isn't available for ${MODE_LABEL[mode]} queries`;
if (!isQueryTypeSupportedByPanelKind(kind, queryType)) {
return `${label} isn't available for ${QUERY_TYPE_LABEL[queryType]} queries`;
}
if (signal !== undefined && !isSignalSupported(kind, signal, mode)) {
if (signal !== undefined && !isSignalSupported(kind, signal)) {
return `${label} doesn't support ${SIGNAL_LABEL[signal]} data`;
}
return undefined;

View File

@@ -58,7 +58,7 @@ function SectionSlot({
signal,
panelKind,
onChangePanelKind,
mode,
queryType,
stepInterval,
metricUnit,
}: SectionSlotProps): JSX.Element | null {
@@ -124,7 +124,7 @@ function SectionSlot({
signal={signal}
panelKind={panelKind}
onChangePanelKind={onChangePanelKind}
mode={mode}
queryType={queryType}
stepInterval={stepInterval}
metricUnit={metricUnit}
registerHeaderAction={registerHeaderAction}

View File

@@ -49,7 +49,7 @@ function renderConfigPane(
spec: spec(),
onChangeSpec: jest.fn(),
onChangePanelKind: jest.fn(),
mode: EQueryType.QUERY_BUILDER,
queryType: EQueryType.QUERY_BUILDER,
legendSeries: [],
tableColumns: [],
panel: { kind: 'Panel', spec: spec() } as DashboardtypesPanelDTO,

View File

@@ -1,9 +1,9 @@
import type { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
import type { PanelQueryMode } from '../../Panels/types/queryModes';
import type { PanelKind } from '../../Panels/types/panelKind';
import type { LegendSeries } from 'pages/DashboardPage/DashboardContainer/Panels/utils/legendSeries';
import type { TableColumnOption } from '../hooks/useTableColumns';
import { EQueryType } from 'types/common/dashboard';
/**
* Context `SectionSlot` forwards to every section editor (not spec-slice fields — those
@@ -17,7 +17,7 @@ export interface SectionEditorContext {
panelKind?: PanelKind;
onChangePanelKind?: (kind: PanelKind) => void;
yAxisUnit?: string;
mode?: PanelQueryMode;
queryType?: EQueryType;
stepInterval?: number;
/** Unit the selected metric was sent with; drives the unit selector's mismatch warning. */
metricUnit?: string;

View File

@@ -16,7 +16,7 @@ import styles from './VisualizationSection.module.scss';
type VisualizationSectionProps = SectionEditorProps<SectionKind.Visualization> &
Pick<
SectionEditorContext,
'panelKind' | 'onChangePanelKind' | 'signal' | 'mode'
'panelKind' | 'onChangePanelKind' | 'signal' | 'queryType'
>;
/**
@@ -31,7 +31,7 @@ function VisualizationSection({
onChange,
panelKind,
onChangePanelKind,
mode,
queryType,
signal,
}: VisualizationSectionProps): JSX.Element {
return (
@@ -39,9 +39,9 @@ function VisualizationSection({
{controls.switchPanelKind && panelKind && onChangePanelKind && (
<PanelTypeSwitcher
panelKind={panelKind}
// mode is optional on the kind-erased section context, but always
// queryType is optional on the kind-erased section context, but always
// supplied in practice; default to Query Builder at this boundary.
mode={mode ?? EQueryType.QUERY_BUILDER}
queryType={queryType ?? EQueryType.QUERY_BUILDER}
signal={signal}
onChange={onChangePanelKind}
/>

View File

@@ -9,11 +9,8 @@ import VisualizationSection from '../VisualizationSection';
jest.mock('pages/DashboardPage/DashboardContainer/Panels/registry', () => ({
getPanelDefinition: jest.fn(() => ({
mode: 'query',
supportedQueryModes: {
builder: { kind: 'signal', signals: ['metrics', 'logs', 'traces'] },
clickhouse_sql: { kind: 'signal-less' },
promql: { kind: 'signal-less' },
},
supportedSignals: ['metrics', 'logs', 'traces'],
supportedQueryTypes: ['builder', 'clickhouse_sql', 'promql'],
})),
PANEL_OPTIONS: [
{ kind: 'signoz/TimeSeriesPanel', displayName: 'Time Series' },

View File

@@ -5,7 +5,7 @@ import {
useMemo,
} from 'react';
import { Color } from '@signozhq/design-tokens';
import { Atom, Sparkles, Terminal } from '@signozhq/icons';
import { Atom, Terminal } from '@signozhq/icons';
import { Tabs } from 'antd';
import cx from 'classnames';
import { Typography } from '@signozhq/ui/typography';
@@ -18,20 +18,12 @@ import PromQLQueryContainer from 'container/QueryBuilder/rawQueryEditors/PromQL'
import RunQueryBtn from 'container/QueryBuilder/components/RunQueryBtn/RunQueryBtn';
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useIsAIObservabilityEnabled } from 'hooks/useIsAIObservabilityEnabled';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { EQueryType } from 'types/common/dashboard';
import { DataSource } from 'types/common/queryBuilder';
import { mergeQueryBuilderFieldRule } from '../../Panels/types/panelCapabilities';
import {
AI_QUERY_MODE,
listQueryModes,
type PanelQueryMode,
} from '../../Panels/types/queryModes';
import type { RenderableQueryPanelDefinition } from '../../Panels/types/panelDefinition';
import { toPanelType } from '../../Panels/types/panelKind';
import { getQueryMode, withQueryMode } from '../../Panels/utils/queryMode';
import styles from './PanelEditorQueryBuilder.module.scss';
@@ -74,18 +66,13 @@ function PanelEditorQueryBuilder({
const isListViewPanel = panelDefinition.kind === 'signoz/ListPanel';
const { currentQuery, redirectWithQueryBuilderData } = useQueryBuilder();
const isDarkMode = useIsDarkMode();
const isAIObservabilityEnabled = useIsAIObservabilityEnabled();
// Derived, not stored: the AI mode is a per-query tag, so the active tab is whatever the
// queries currently say. A mode the kind no longer offers (after a kind switch) falls back
// to the builder rather than selecting a tab that isn't rendered.
const activeMode = getQueryMode(currentQuery);
const handleQueryCategoryChange = useCallback(
(mode: string): void => {
redirectWithQueryBuilderData(
withQueryMode(currentQuery, mode as PanelQueryMode),
);
(queryType: string): void => {
redirectWithQueryBuilderData({
...currentQuery,
queryType: queryType as EQueryType,
});
},
[currentQuery, redirectWithQueryBuilderData],
);
@@ -112,9 +99,7 @@ function PanelEditorQueryBuilder({
);
const items = useMemo(() => {
const supportedModes = listQueryModes(
panelDefinition.supportedQueryModes,
).filter((mode) => mode !== AI_QUERY_MODE || isAIObservabilityEnabled);
const { supportedQueryTypes } = panelDefinition;
const queryTypeComponents = {
[EQueryType.QUERY_BUILDER]: {
@@ -149,48 +134,19 @@ function PanelEditorQueryBuilder({
label: 'PromQL',
component: <PromQLQueryContainer />,
},
// Traces only, and the source selector is hidden with it: an AI query that moved
// off traces is no longer an AI query. `queryVariant: 'static'` matches the AI
// explorer's builder.
[AI_QUERY_MODE]: {
icon: <Sparkles size={14} />,
label: 'AI Query Builder',
component: (
<div className="query-builder-v2-container">
<QueryBuilderV2
panelType={panelType}
filterConfigs={filterConfigs}
showTraceOperator={false}
version="v3"
queryComponents={{}}
config={{
initialDataSource: DataSource.TRACES,
queryVariant: 'static',
}}
/>
</div>
),
},
};
return supportedModes.map((mode) => ({
key: mode,
return supportedQueryTypes.map((queryType) => ({
key: queryType,
label: (
<div className={styles.queryTypeTab}>
{queryTypeComponents[mode].icon}
<Typography>{queryTypeComponents[mode].label}</Typography>
{queryTypeComponents[queryType].icon}
<Typography>{queryTypeComponents[queryType].label}</Typography>
</div>
),
children: queryTypeComponents[mode].component,
children: queryTypeComponents[queryType].component,
}));
}, [
panelDefinition,
panelType,
filterConfigs,
isDarkMode,
isListViewPanel,
isAIObservabilityEnabled,
]);
}, [panelDefinition, panelType, filterConfigs, isDarkMode, isListViewPanel]);
return (
<div
@@ -205,7 +161,7 @@ function PanelEditorQueryBuilder({
className={cx(styles.tabsContainer, {
[styles.stickyNav]: stickyHeader,
})}
activeKey={activeMode}
activeKey={currentQuery.queryType}
onChange={handleQueryCategoryChange}
tabBarExtraContent={
<span className={styles.runQueryBtnContainer}>

View File

@@ -12,10 +12,6 @@ import PanelEditorQueryBuilder from '../PanelEditorQueryBuilder';
// Capture the props the (real-guard-fed) QueryBuilderV2 receives without rendering it.
const mockQueryBuilderV2 = jest.fn();
jest.mock('hooks/useIsAIObservabilityEnabled', () => ({
useIsAIObservabilityEnabled: (): boolean => false,
}));
jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({
useQueryBuilder: jest.fn(),
}));

View File

@@ -21,7 +21,6 @@ import { useDashboardEditContext } from '../hooks/useDashboardEditContext';
import { getExecStats } from '../queryV5/v5ResponseData';
import { usePanelInteractions } from '../PanelsAndSectionsLayout/Panel/hooks/usePanelInteractions';
import { useScrollIntoViewStore } from '../store/useScrollIntoViewStore';
import { getQueryMode } from '../Panels/utils/queryMode';
import ConfigPane from './ConfigPane/ConfigPane';
import Header from './Header/Header';
import PanelEditorLayout, {
@@ -320,7 +319,7 @@ function QueryEditorBody({
spec={spec}
onChangeSpec={setSpec}
onChangePanelKind={onChangePanelKind}
mode={getQueryMode(currentQuery)}
queryType={currentQuery.queryType}
legendSeries={legendSeries}
tableColumns={tableColumns}
stepInterval={stepInterval}

View File

@@ -122,7 +122,7 @@ function StaticEditorBody({
spec={spec}
onChangeSpec={setSpec}
onChangePanelKind={onChangePanelKind}
mode={EQueryType.QUERY_BUILDER}
queryType={EQueryType.QUERY_BUILDER}
legendSeries={[]}
tableColumns={[]}
/>

View File

@@ -23,10 +23,6 @@ import { requireQueryPanelDefinition } from 'pages/DashboardPage/DashboardContai
import PanelEditorQueryBuilder from '../PanelEditorQueryBuilder/PanelEditorQueryBuilder';
// jest.config maps the real hook to a no-op mock; this suite needs real navigation.
jest.mock('hooks/useIsAIObservabilityEnabled', () => ({
useIsAIObservabilityEnabled: (): boolean => false,
}));
jest.mock('hooks/useSafeNavigate', () => {
const { useHistory: useRouterHistory } =
jest.requireActual('react-router-dom');

View File

@@ -5,7 +5,7 @@ import { handleQueryChange } from 'lib/query/panelQuery';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import type { Query } from 'types/api/queryBuilder/queryBuilderData';
import { resolveQueryMode } from '../../../Panels/capabilities';
import { resolveQueryType } from '../../../Panels/capabilities';
import { getBuilderQueries } from '../../../Panels/utils/getBuilderQueries';
import { toPerses } from '../../../queryV5/persesQueryAdapters';
import { getSwitchedPluginSpec } from '../../getSwitchedPluginSpec';
@@ -18,7 +18,7 @@ jest.mock('lib/query/panelQuery', () => ({
handleQueryChange: jest.fn(),
}));
jest.mock('../../../Panels/capabilities', () => ({
resolveQueryMode: jest.fn(),
resolveQueryType: jest.fn(),
// Real predicate: these specs use real (query) kinds and the static path is
// exercised through its own cases below.
isStaticPanelKind: jest.requireActual('../../../Panels/capabilities')
@@ -36,7 +36,7 @@ jest.mock('../../../Panels/utils/getBuilderQueries', () => ({
const mockUseQueryBuilder = useQueryBuilder as unknown as jest.Mock;
const mockHandleQueryChange = handleQueryChange as unknown as jest.Mock;
const mockResolveQueryMode = resolveQueryMode as unknown as jest.Mock;
const mockResolveQueryType = resolveQueryType as unknown as jest.Mock;
const mockToPerses = toPerses as unknown as jest.Mock;
const mockGetSwitchedPluginSpec = getSwitchedPluginSpec as unknown as jest.Mock;
const mockGetBuilderQueries = getBuilderQueries as unknown as jest.Mock;
@@ -95,7 +95,7 @@ describe('usePanelTypeSwitch', () => {
mockGetBuilderQueries.mockReturnValue([{ signal: 'logs' }]);
// The guard owns coercion (tested in capabilities.test.ts); here it always
// resolves to Query Builder so the coerced type flows into handleQueryChange.
mockResolveQueryMode.mockReturnValue('builder');
mockResolveQueryType.mockReturnValue('builder');
});
it('does nothing when switching to the current kind', () => {
@@ -185,54 +185,16 @@ describe('usePanelTypeSwitch', () => {
);
act(() => result.current.onChangePanelKind('signoz/ListPanel'));
// The hook asks the guard to resolve the active mode against the new kind, passing
// the current signal — the AI mode is only valid for traces.
expect(mockResolveQueryMode).toHaveBeenCalledWith(
// The hook asks the guard to resolve the active query type against the new kind
expect(mockResolveQueryType).toHaveBeenCalledWith(
'signoz/ListPanel',
'promql',
'logs',
);
// …and the resolved mode ('builder') flows into the query rebuild.
// …and the resolved type ('builder') flows into the query rebuild.
const [, queryArg] = mockHandleQueryChange.mock.calls[0];
expect((queryArg as Query).queryType).toBe('builder');
});
it('strips the AI tag when the new kind has no AI mode', () => {
const setSpec = jest.fn();
const aiQuery = {
id: 'ai',
queryType: 'builder',
builder: {
queryData: [{ dataSource: 'traces', builderQueryType: 'builder_ai_query' }],
queryFormulas: [],
queryTraceOperator: [],
},
} as unknown as Query;
mockUseQueryBuilder.mockReturnValue(builderState(aiQuery));
// List declares no AI mode, so the guard coerces back to the builder.
mockResolveQueryMode.mockReturnValue('builder');
const { result } = renderHook(() =>
usePanelTypeSwitch({
spec: makeSpec('signoz/TimeSeriesPanel', {}, TABLE_QUERIES),
panelType: PANEL_TYPES.TIME_SERIES,
setSpec,
}),
);
act(() => result.current.onChangePanelKind('signoz/ListPanel'));
expect(mockResolveQueryMode).toHaveBeenCalledWith(
'signoz/ListPanel',
'builder_ai_query',
'logs',
);
// The tag has to come off the queries themselves — queryType alone can't say it.
const [, queryArg] = mockHandleQueryChange.mock.calls[0];
(queryArg as Query).builder.queryData.forEach((qd) => {
expect(qd.builderQueryType).toBeUndefined();
});
});
it('restores the original kind verbatim on switch-back (reversibility)', () => {
const setSpec = jest.fn();
const tableQuery = { id: 'table-current', queryType: 'builder' } as Query;

View File

@@ -4,10 +4,7 @@ import type {
TelemetrytypesSignalDTO,
} from 'api/generated/services/sigNoz.schemas';
import type { PANEL_TYPES } from 'constants/queryBuilder';
import {
getSupportedSignals,
requireQueryPanelDefinition,
} from 'pages/DashboardPage/DashboardContainer/Panels/capabilities';
import { requireQueryPanelDefinition } from 'pages/DashboardPage/DashboardContainer/Panels/capabilities';
import { isPanelKindSupported } from 'pages/DashboardPage/DashboardContainer/Panels/registry';
import type { RenderableQueryPanelDefinition } from 'pages/DashboardPage/DashboardContainer/Panels/types/panelDefinition';
import { toPanelType } from 'pages/DashboardPage/DashboardContainer/Panels/types/panelKind';
@@ -94,7 +91,7 @@ export function usePanelEditSession({
// View modal shells) — asserted rather than assumed.
const panelDefinition = requireQueryPanelDefinition(panelKind);
const panelType = toPanelType(panelKind);
const defaultSignal = getSupportedSignals(panelKind)[0];
const defaultSignal = panelDefinition.supportedSignals[0];
const query = usePanelQuery({
panel: draft,

View File

@@ -18,8 +18,7 @@ import type {
Query,
} from 'types/api/queryBuilder/queryBuilderData';
import { isStaticPanelKind, resolveQueryMode } from '../../Panels/capabilities';
import { getQueryMode, withQueryMode } from '../../Panels/utils/queryMode';
import { isStaticPanelKind, resolveQueryType } from '../../Panels/capabilities';
import { toPanelType, type PanelKind } from '../../Panels/types/panelKind';
import { getBuilderQueries } from '../../Panels/utils/getBuilderQueries';
import { toPerses } from '../../queryV5/persesQueryAdapters';
@@ -148,16 +147,12 @@ export function usePanelTypeSwitch({
return;
}
// First visit → coerce the mode if the new kind disallows it, then rebuild the
// builder query for it. Going through the mode (not `queryType`) is what lets an
// AI query lose its per-query tag when the new kind has no AI mode: `queryType`
// alone cannot express that.
const currentSignal = getBuilderQueries(currentSpec.queries)[0]
?.signal as TelemetrytypesSignalDTO;
const mode = resolveQueryMode(newKind, getQueryMode(query), currentSignal);
// First visit → coerce the query type if the new kind disallows it, then
// rebuild the builder query for the new type.
const queryType = resolveQueryType(newKind, query.queryType);
const transformed = handleQueryChange(
newPanelType as keyof PartialPanelTypes,
withQueryMode(query, mode),
{ ...query, queryType },
panelTypeRef.current,
);
// Match a fresh list panel's default order so the builder's Order By isn't empty.
@@ -165,9 +160,12 @@ export function usePanelTypeSwitch({
newKind === 'signoz/ListPanel'
? withDefaultListOrder(transformed)
: transformed;
const signal = getBuilderQueries(currentSpec.queries)[0]
?.signal as TelemetrytypesSignalDTO;
setSpec(
buildSpec(
getSwitchedPluginSpec(currentSpec, newKind, currentSignal),
getSwitchedPluginSpec(currentSpec, newKind, signal),
toPerses(nextQuery, newPanelType),
),
);

View File

@@ -17,12 +17,9 @@ import {
getSupportedSignals,
isPanelCombinationValid,
isQueryTypeSupportedByPanelKind,
isQueryModeSupportedByPanelKind,
isSignalSupported,
resolveQueryMode,
resolveQueryType,
} from '../capabilities';
import { AI_QUERY_MODE } from '../types/queryModes';
import type { PanelKind } from '../types/panelKind';
const { QUERY_BUILDER, CLICKHOUSE, PROM } = EQueryType;
@@ -145,7 +142,7 @@ describe('panel capabilities guard', () => {
expect(getSupportedQueryTypes(unknownKind)).toStrictEqual([]);
expect(isSignalSupported(unknownKind, logs)).toBe(false);
expect(
isPanelCombinationValid({ kind: unknownKind, mode: QUERY_BUILDER }),
isPanelCombinationValid({ kind: unknownKind, queryType: QUERY_BUILDER }),
).toBe(false);
expect(getHiddenQueryBuilderFields(unknownKind, logs)).toStrictEqual({});
expect(getPanelDefinition(unknownKind).sections).toStrictEqual([]);
@@ -215,13 +212,13 @@ describe('panel capabilities guard', () => {
expect(
isPanelCombinationValid({
kind: 'signoz/TimeSeriesPanel',
mode: PROM,
queryType: PROM,
}),
).toBe(true);
expect(
isPanelCombinationValid({
kind: 'signoz/ListPanel',
mode: QUERY_BUILDER,
queryType: QUERY_BUILDER,
signal: logs,
}),
).toBe(true);
@@ -229,10 +226,10 @@ describe('panel capabilities guard', () => {
it('rejects an unsupported query type', () => {
expect(
isPanelCombinationValid({ kind: 'signoz/ListPanel', mode: PROM }),
isPanelCombinationValid({ kind: 'signoz/ListPanel', queryType: PROM }),
).toBe(false);
expect(
isPanelCombinationValid({ kind: 'signoz/TablePanel', mode: PROM }),
isPanelCombinationValid({ kind: 'signoz/TablePanel', queryType: PROM }),
).toBe(false);
});
@@ -240,7 +237,7 @@ describe('panel capabilities guard', () => {
expect(
isPanelCombinationValid({
kind: 'signoz/ListPanel',
mode: QUERY_BUILDER,
queryType: QUERY_BUILDER,
signal: metrics,
}),
).toBe(false);
@@ -250,7 +247,7 @@ describe('panel capabilities guard', () => {
expect(
isPanelCombinationValid({
kind: 'signoz/ListPanel',
mode: QUERY_BUILDER,
queryType: QUERY_BUILDER,
}),
).toBe(true);
});
@@ -271,85 +268,6 @@ describe('panel capabilities guard', () => {
});
});
describe('the AI query mode', () => {
const AI_KINDS: PanelKind[] = [
'signoz/TimeSeriesPanel',
'signoz/BarChartPanel',
'signoz/NumberPanel',
'signoz/HistogramPanel',
'signoz/PieChartPanel',
'signoz/TablePanel',
];
it.each(AI_KINDS)('is offered by %s, for traces only', (kind) => {
expect(isQueryModeSupportedByPanelKind(kind, AI_QUERY_MODE)).toBe(true);
expect(getSupportedSignals(kind, AI_QUERY_MODE)).toStrictEqual([traces]);
});
it('is not offered by List, whose raw rows carry no aggregation', () => {
expect(
isQueryModeSupportedByPanelKind('signoz/ListPanel', AI_QUERY_MODE),
).toBe(false);
});
it('does not leak into the legacy queryType axis', () => {
expect(getSupportedQueryTypes('signoz/TimeSeriesPanel')).not.toContain(
AI_QUERY_MODE,
);
});
it('leaves the kind-wide signal list unchanged', () => {
// traces is already in the builder mode's list, so the union must not repeat it.
expect(getSupportedSignals('signoz/TimeSeriesPanel')).toStrictEqual([
metrics,
logs,
traces,
]);
});
it('pairs with traces but not with the other signals of the kind', () => {
expect(
isPanelCombinationValid({
kind: 'signoz/TimeSeriesPanel',
mode: AI_QUERY_MODE,
signal: traces,
}),
).toBe(true);
expect(
isPanelCombinationValid({
kind: 'signoz/TimeSeriesPanel',
mode: AI_QUERY_MODE,
signal: logs,
}),
).toBe(false);
});
});
describe('resolveQueryMode', () => {
it('keeps the AI mode on a kind that offers it', () => {
expect(
resolveQueryMode('signoz/TimeSeriesPanel', AI_QUERY_MODE, traces),
).toBe(AI_QUERY_MODE);
});
it('coerces the AI mode on a kind that does not offer it', () => {
expect(resolveQueryMode('signoz/ListPanel', AI_QUERY_MODE, traces)).toBe(
QUERY_BUILDER,
);
});
it('coerces the AI mode when the signal moved off traces', () => {
expect(resolveQueryMode('signoz/TimeSeriesPanel', AI_QUERY_MODE, logs)).toBe(
QUERY_BUILDER,
);
});
it('leaves a query-language mode alone', () => {
expect(resolveQueryMode('signoz/TimeSeriesPanel', PROM)).toBe(PROM);
expect(resolveQueryMode('signoz/ListPanel', PROM)).toBe(QUERY_BUILDER);
});
});
describe('getHiddenQueryBuilderFields', () => {
it('returns {} for kinds that declare no field rules', () => {
expect(

View File

@@ -8,16 +8,10 @@ import {
} from './types/panelCapabilities';
import type { RenderableQueryPanelDefinition } from './types/panelDefinition';
import type { PanelKind } from './types/panelKind';
import {
listQueryModes,
listQueryTypes,
signalsForMode,
type PanelQueryMode,
} from './types/queryModes';
/**
* The single deterministic guard for V2 dashboards. Every "what works with what"
* question — panel kind × query mode × signal, and which query-builder fields a kind
* question — panel kind × query type × signal, and which query-builder fields a kind
* hides — is answered here by reading each kind's declared capabilities from the panel
* registry. Adding a new kind means declaring its capabilities once in its definition;
* these functions then cover it automatically. Pure and side-effect free.
@@ -58,47 +52,23 @@ export function requireQueryPanelDefinition(
return definition;
}
/** Every mode this kind offers, in declaration order (the builder first). */
export function getSupportedQueryModes(kind: PanelKind): PanelQueryMode[] {
const definition = getQueryPanelDefinition(kind);
return definition ? listQueryModes(definition.supportedQueryModes) : [];
}
export function isQueryModeSupportedByPanelKind(
kind: PanelKind,
mode: PanelQueryMode,
): boolean {
return getSupportedQueryModes(kind).includes(mode);
}
/**
* Signals a kind can visualize — in `mode` when one is given, else across every mode it
* offers. The mode-scoped form is what keeps the two axes interoperable: the AI mode
* authors traces only, on a kind whose builder mode also takes logs and metrics.
*/
/** Signals a kind can visualize. */
export function getSupportedSignals(
kind: PanelKind,
mode?: PanelQueryMode,
): TelemetrytypesSignalDTO[] {
const modes = getQueryPanelDefinition(kind)?.supportedQueryModes;
return modes ? signalsForMode(modes, mode) : [];
return getQueryPanelDefinition(kind)?.supportedSignals ?? [];
}
export function isSignalSupported(
kind: PanelKind,
signal: TelemetrytypesSignalDTO,
mode?: PanelQueryMode,
): boolean {
return getSupportedSignals(kind, mode).includes(signal);
return getSupportedSignals(kind).includes(signal);
}
/**
* Query languages a kind supports (Query Builder / ClickHouse / PromQL) — its modes
* minus the AI one, for the call sites that speak the legacy `queryType` axis.
*/
/** Query languages a kind supports (Query Builder / ClickHouse / PromQL). */
export function getSupportedQueryTypes(kind: PanelKind): EQueryType[] {
const definition = getQueryPanelDefinition(kind);
return definition ? listQueryTypes(definition.supportedQueryModes) : [];
return getQueryPanelDefinition(kind)?.supportedQueryTypes ?? [];
}
export function isQueryTypeSupportedByPanelKind(
@@ -109,63 +79,48 @@ export function isQueryTypeSupportedByPanelKind(
}
/**
* Master guard: is this panel kind renderable in this mode (and, where the mode carries
* a signal, with this signal)? ClickHouse/PromQL queries carry no signal, so the signal
* is validated only when one is given.
* Master guard: is this panel kind renderable with this query type (and, in builder
* mode, this signal)? ClickHouse/PromQL queries carry no signal, so the signal is
* validated only when one is given.
*/
export function isPanelCombinationValid({
kind,
mode,
queryType,
signal,
}: {
kind: PanelKind;
mode: PanelQueryMode;
queryType: EQueryType;
signal?: TelemetrytypesSignalDTO;
}): boolean {
// A query-less kind ignores the query entirely, so it pairs with anything.
if (isStaticPanelKind(kind)) {
return true;
}
if (!isQueryModeSupportedByPanelKind(kind, mode)) {
if (!isQueryTypeSupportedByPanelKind(kind, queryType)) {
return false;
}
if (signal !== undefined && !isSignalSupported(kind, signal, mode)) {
if (signal !== undefined && !isSignalSupported(kind, signal)) {
return false;
}
return true;
}
/**
* The mode to use for a kind given a `preferred` one: keep it if the kind offers it and
* it admits the signal, otherwise fall back to the kind's first mode. Used when switching
* panel kinds to coerce an unsupported active mode (PromQL → a List panel coerces to Query
* Builder; AI → a kind with no AI mode does the same).
* The query type to use for a kind given a `preferred` one: keep it if the kind
* supports it, otherwise fall back to the kind's first supported type. Used when
* switching panel kinds to coerce an unsupported active query type (e.g. PromQL → a
* List panel coerces to Query Builder).
*/
export function resolveQueryMode(
kind: PanelKind,
preferred: PanelQueryMode,
signal?: TelemetrytypesSignalDTO,
): PanelQueryMode {
const supported = getSupportedQueryModes(kind);
if (
supported.includes(preferred) &&
(signal === undefined || isSignalSupported(kind, signal, preferred))
) {
return preferred;
}
// A query-less kind has no modes; the builder is the neutral answer.
return supported[0] ?? EQueryType.QUERY_BUILDER;
}
/** `resolveQueryMode` narrowed to the legacy `queryType` axis. */
export function resolveQueryType(
kind: PanelKind,
preferred: EQueryType,
): EQueryType {
const supported = getSupportedQueryTypes(kind);
return supported.includes(preferred)
? preferred
: (supported[0] ?? EQueryType.QUERY_BUILDER);
if (supported.includes(preferred)) {
return preferred;
}
// A query-less kind has no supported types; the builder is the neutral answer.
return supported[0] ?? EQueryType.QUERY_BUILDER;
}
/**

View File

@@ -10,8 +10,6 @@ import {
} from 'api/generated/services/sigNoz.schemas';
import { EQueryType } from 'types/common/dashboard';
import { AI_QUERY_MODE } from '../../types/queryModes';
export const definition: PanelDefinition<'signoz/BarChartPanel'> = {
kind: 'signoz/BarChartPanel',
displayName: 'Bar Chart',
@@ -20,22 +18,16 @@ export const definition: PanelDefinition<'signoz/BarChartPanel'> = {
Renderer,
EditorPane: QueryBuilderEditorPane,
sections,
supportedQueryModes: {
[EQueryType.QUERY_BUILDER]: {
kind: 'signal',
signals: [
TelemetrytypesSignalDTO.metrics,
TelemetrytypesSignalDTO.logs,
TelemetrytypesSignalDTO.traces,
],
},
[EQueryType.CLICKHOUSE]: { kind: 'signal-less' },
[EQueryType.PROM]: { kind: 'signal-less' },
[AI_QUERY_MODE]: {
kind: 'signal',
signals: [TelemetrytypesSignalDTO.traces],
},
},
supportedSignals: [
TelemetrytypesSignalDTO.metrics,
TelemetrytypesSignalDTO.logs,
TelemetrytypesSignalDTO.traces,
],
supportedQueryTypes: [
EQueryType.QUERY_BUILDER,
EQueryType.CLICKHOUSE,
EQueryType.PROM,
],
queryBuilderFields: {},
// Bars are binned client-side from a raw time series, so the request asks for a
// step interval wide enough to keep the bar count readable (V1 parity).

View File

@@ -10,8 +10,6 @@ import {
} from 'api/generated/services/sigNoz.schemas';
import { EQueryType } from 'types/common/dashboard';
import { AI_QUERY_MODE } from '../../types/queryModes';
export const definition: PanelDefinition<'signoz/HistogramPanel'> = {
kind: 'signoz/HistogramPanel',
displayName: 'Histogram',
@@ -20,22 +18,16 @@ export const definition: PanelDefinition<'signoz/HistogramPanel'> = {
Renderer,
EditorPane: QueryBuilderEditorPane,
sections,
supportedQueryModes: {
[EQueryType.QUERY_BUILDER]: {
kind: 'signal',
signals: [
TelemetrytypesSignalDTO.metrics,
TelemetrytypesSignalDTO.logs,
TelemetrytypesSignalDTO.traces,
],
},
[EQueryType.CLICKHOUSE]: { kind: 'signal-less' },
[EQueryType.PROM]: { kind: 'signal-less' },
[AI_QUERY_MODE]: {
kind: 'signal',
signals: [TelemetrytypesSignalDTO.traces],
},
},
supportedSignals: [
TelemetrytypesSignalDTO.metrics,
TelemetrytypesSignalDTO.logs,
TelemetrytypesSignalDTO.traces,
],
supportedQueryTypes: [
EQueryType.QUERY_BUILDER,
EQueryType.CLICKHOUSE,
EQueryType.PROM,
],
queryBuilderFields: {},
// Buckets are computed client-side from the raw series, so the request is a plain
// time series — the bucket count is a display concern, not a query one.

View File

@@ -18,18 +18,16 @@ export const definition: PanelDefinition<'signoz/ListPanel'> = {
icon: List,
Renderer,
EditorPane: ListEditorPane,
// Raw records come from logs and traces; metrics don't produce row data. No AI mode:
// the AI builder authors aggregations, which raw rows have no place for.
supportedQueryModes: {
[EQueryType.QUERY_BUILDER]: {
kind: 'signal',
signals: [TelemetrytypesSignalDTO.logs, TelemetrytypesSignalDTO.traces],
},
},
// Raw records come from logs and traces; metrics don't produce row data.
supportedSignals: [
TelemetrytypesSignalDTO.logs,
TelemetrytypesSignalDTO.traces,
],
// Raw rows have no aggregation, so step interval / having never apply, and the
// Where clause searches the log/span body via `body CONTAINS`. Traces additionally
// hide `limit` (the server paginates raw spans). Mirrors QueryBuilderV2's internal
// list configs — the capabilities guard is the single source for both.
supportedQueryTypes: [EQueryType.QUERY_BUILDER],
queryBuilderFields: {
default: {
stepInterval: { isHidden: true, isDisabled: true },

View File

@@ -10,8 +10,6 @@ import {
} from 'api/generated/services/sigNoz.schemas';
import { EQueryType } from 'types/common/dashboard';
import { AI_QUERY_MODE } from '../../types/queryModes';
export const definition: PanelDefinition<'signoz/NumberPanel'> = {
kind: 'signoz/NumberPanel',
displayName: 'Number',
@@ -20,22 +18,16 @@ export const definition: PanelDefinition<'signoz/NumberPanel'> = {
Renderer,
EditorPane: QueryBuilderEditorPane,
sections,
supportedQueryModes: {
[EQueryType.QUERY_BUILDER]: {
kind: 'signal',
signals: [
TelemetrytypesSignalDTO.metrics,
TelemetrytypesSignalDTO.logs,
TelemetrytypesSignalDTO.traces,
],
},
[EQueryType.CLICKHOUSE]: { kind: 'signal-less' },
[EQueryType.PROM]: { kind: 'signal-less' },
[AI_QUERY_MODE]: {
kind: 'signal',
signals: [TelemetrytypesSignalDTO.traces],
},
},
supportedSignals: [
TelemetrytypesSignalDTO.metrics,
TelemetrytypesSignalDTO.logs,
TelemetrytypesSignalDTO.traces,
],
supportedQueryTypes: [
EQueryType.QUERY_BUILDER,
EQueryType.CLICKHOUSE,
EQueryType.PROM,
],
queryBuilderFields: {},
queryCapabilities: {
requestType: Querybuildertypesv5RequestTypeDTO.scalar,

View File

@@ -10,8 +10,6 @@ import {
} from 'api/generated/services/sigNoz.schemas';
import { EQueryType } from 'types/common/dashboard';
import { AI_QUERY_MODE } from '../../types/queryModes';
export const definition: PanelDefinition<'signoz/PieChartPanel'> = {
kind: 'signoz/PieChartPanel',
displayName: 'Pie Chart',
@@ -20,21 +18,12 @@ export const definition: PanelDefinition<'signoz/PieChartPanel'> = {
Renderer,
EditorPane: QueryBuilderEditorPane,
sections,
supportedQueryModes: {
[EQueryType.QUERY_BUILDER]: {
kind: 'signal',
signals: [
TelemetrytypesSignalDTO.metrics,
TelemetrytypesSignalDTO.logs,
TelemetrytypesSignalDTO.traces,
],
},
[EQueryType.CLICKHOUSE]: { kind: 'signal-less' },
[AI_QUERY_MODE]: {
kind: 'signal',
signals: [TelemetrytypesSignalDTO.traces],
},
},
supportedSignals: [
TelemetrytypesSignalDTO.metrics,
TelemetrytypesSignalDTO.logs,
TelemetrytypesSignalDTO.traces,
],
supportedQueryTypes: [EQueryType.QUERY_BUILDER, EQueryType.CLICKHOUSE],
queryBuilderFields: {},
queryCapabilities: {
requestType: Querybuildertypesv5RequestTypeDTO.scalar,

View File

@@ -10,8 +10,6 @@ import {
} from 'api/generated/services/sigNoz.schemas';
import { EQueryType } from 'types/common/dashboard';
import { AI_QUERY_MODE } from '../../types/queryModes';
export const definition: PanelDefinition<'signoz/TablePanel'> = {
kind: 'signoz/TablePanel',
displayName: 'Table',
@@ -20,21 +18,12 @@ export const definition: PanelDefinition<'signoz/TablePanel'> = {
Renderer,
EditorPane: QueryBuilderEditorPane,
sections,
supportedQueryModes: {
[EQueryType.QUERY_BUILDER]: {
kind: 'signal',
signals: [
TelemetrytypesSignalDTO.metrics,
TelemetrytypesSignalDTO.logs,
TelemetrytypesSignalDTO.traces,
],
},
[EQueryType.CLICKHOUSE]: { kind: 'signal-less' },
[AI_QUERY_MODE]: {
kind: 'signal',
signals: [TelemetrytypesSignalDTO.traces],
},
},
supportedSignals: [
TelemetrytypesSignalDTO.metrics,
TelemetrytypesSignalDTO.logs,
TelemetrytypesSignalDTO.traces,
],
supportedQueryTypes: [EQueryType.QUERY_BUILDER, EQueryType.CLICKHOUSE],
queryBuilderFields: {},
// The only kind that asks the server to transpose its scalar result into UI rows.
queryCapabilities: {

View File

@@ -10,8 +10,6 @@ import {
} from 'api/generated/services/sigNoz.schemas';
import { EQueryType } from 'types/common/dashboard';
import { AI_QUERY_MODE } from '../../types/queryModes';
export const definition: PanelDefinition<'signoz/TimeSeriesPanel'> = {
kind: 'signoz/TimeSeriesPanel',
displayName: 'Time Series',
@@ -20,22 +18,16 @@ export const definition: PanelDefinition<'signoz/TimeSeriesPanel'> = {
Renderer,
EditorPane: QueryBuilderEditorPane,
sections,
supportedQueryModes: {
[EQueryType.QUERY_BUILDER]: {
kind: 'signal',
signals: [
TelemetrytypesSignalDTO.metrics,
TelemetrytypesSignalDTO.logs,
TelemetrytypesSignalDTO.traces,
],
},
[EQueryType.CLICKHOUSE]: { kind: 'signal-less' },
[EQueryType.PROM]: { kind: 'signal-less' },
[AI_QUERY_MODE]: {
kind: 'signal',
signals: [TelemetrytypesSignalDTO.traces],
},
},
supportedSignals: [
TelemetrytypesSignalDTO.metrics,
TelemetrytypesSignalDTO.logs,
TelemetrytypesSignalDTO.traces,
],
supportedQueryTypes: [
EQueryType.QUERY_BUILDER,
EQueryType.CLICKHOUSE,
EQueryType.PROM,
],
queryBuilderFields: {},
queryCapabilities: {
requestType: Querybuildertypesv5RequestTypeDTO.time_series,

View File

@@ -25,7 +25,8 @@ export const UNSUPPORTED_PANEL: RenderablePanelDefinition = {
Renderer,
EditorPane: QueryBuilderEditorPane,
sections: [],
supportedQueryModes: {},
supportedSignals: [],
supportedQueryTypes: [],
queryBuilderFields: {},
queryCapabilities: {
requestType: Querybuildertypesv5RequestTypeDTO.time_series,

View File

@@ -4,6 +4,7 @@ import {
TelemetrytypesSignalDTO,
} from 'api/generated/services/sigNoz.schemas';
import type { ChartLine } from '@signozhq/icons';
import type { EQueryType } from 'types/common/dashboard';
import type { SectionConfig } from './sections';
import type { AnyPanelInteractionProps } from './interactions';
@@ -12,7 +13,6 @@ import type {
PanelQueryCapabilities,
QueryBuilderFieldRule,
} from './panelCapabilities';
import type { SupportedQueryModes } from './queryModes';
import type {
BaseRendererProps,
PanelRendererProps,
@@ -114,8 +114,10 @@ export interface QueryPanelDefinition<
Renderer: ComponentType<PanelRendererProps<K>>;
/** Lower editor pane — the shared query-builder pane, or a kind wrapper of it. */
EditorPane: ComponentType<QueryEditorPaneProps>;
/** Modes this kind offers, each with the signals authorable in it. */
supportedQueryModes: SupportedQueryModes;
/** Signals this kind can visualize. */
supportedSignals: TelemetrytypesSignalDTO[];
/** Query languages this kind supports (Query Builder / ClickHouse / PromQL). */
supportedQueryTypes: EQueryType[];
/** Query-builder fields this kind hides/disables, optionally per signal (`{}` hides none). */
queryBuilderFields: QueryBuilderFieldRule;
/** How this kind's query-range request is shaped (request type, paging, result formatting). */

View File

@@ -1,66 +0,0 @@
import type { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
import type { QueryType } from 'types/api/v5/queryRange';
import type { EQueryType } from 'types/common/dashboard';
/**
* The query-authoring modes a panel kind can offer, one per editor tab. Three are query
* languages (`EQueryType`); the fourth is the AI query builder, which is a builder query
* under the hood and so has no `EQueryType` of its own — it is keyed by the v5 query type
* stamped on each of its queries (`IBuilderQuery.builderQueryType`), which is what lets
* the active tab be read straight off the query.
*/
export type PanelQueryMode =
| EQueryType
| Extract<QueryType, 'builder_ai_query'>;
/** The AI query builder's mode key. */
export const AI_QUERY_MODE = 'builder_ai_query' as const;
/**
* What a mode authors: a set of signals, or nothing signal-shaped at all. ClickHouse and
* PromQL are raw query text and carry no signal, which is a different statement from "this
* mode takes an empty set of signals" — the union makes the two unconfusable.
*/
export type QueryModeCapability =
| { kind: 'signal'; signals: TelemetrytypesSignalDTO[] }
| { kind: 'signal-less' };
/**
* Every mode a kind offers, with what each one authors. Partial on purpose: an absent key
* means the kind does not offer that mode at all (no tab, and the kind is disabled while that
* mode is active), which is how List opts out of everything but the builder.
*
* Declaring modes and signals together is what keeps them interoperable: AI is traces-only on
* a kind whose builder mode also takes logs and metrics — which two independent lists can't
* express.
*/
export type SupportedQueryModes = Partial<
Record<PanelQueryMode, QueryModeCapability>
>;
/** Declaration order, which puts the builder first. */
export function listQueryModes(modes: SupportedQueryModes): PanelQueryMode[] {
return Object.keys(modes) as PanelQueryMode[];
}
/** The modes that are query languages, for call sites on the legacy `queryType` axis. */
export function listQueryTypes(modes: SupportedQueryModes): EQueryType[] {
return listQueryModes(modes).filter(
(mode): mode is EQueryType => mode !== AI_QUERY_MODE,
);
}
/** Signals authorable in `mode`, or across every mode when none is given. */
export function signalsForMode(
modes: SupportedQueryModes,
mode?: PanelQueryMode,
): TelemetrytypesSignalDTO[] {
if (mode) {
const capability = modes[mode];
return capability?.kind === 'signal' ? capability.signals : [];
}
const all = Object.values(modes).flatMap((capability) =>
capability?.kind === 'signal' ? capability.signals : [],
);
return [...new Set(all)];
}

View File

@@ -4,11 +4,6 @@ import type {
} from 'api/generated/services/sigNoz.schemas';
import type { BuilderQuery } from 'types/api/v5/queryRange';
import {
isBuilderEnvelope,
isBuilderPluginKind,
} from '../../queryV5/builderEnvelope';
/**
* Flattens a panel's queries into its builder queries, unwrapping
* `CompositeQuery` envelopes. Non-builder kinds (PromQL, ClickHouseSQL, Formula,
@@ -21,13 +16,13 @@ export function getBuilderQueries(
const flattened: BuilderQuery[] = [];
queries.forEach((envelope) => {
const plugin = envelope.spec.plugin;
if (isBuilderPluginKind(plugin.kind)) {
if (plugin.kind === 'signoz/BuilderQuery') {
flattened.push(plugin.spec as BuilderQuery);
return;
}
if (plugin.kind === 'signoz/CompositeQuery') {
(plugin.spec.queries || []).forEach((sub) => {
if (isBuilderEnvelope(sub)) {
if (sub.type === 'builder_query') {
flattened.push(sub.spec as BuilderQuery);
}
});

View File

@@ -2,7 +2,7 @@ import type { DashboardtypesPanelDTO } from 'api/generated/services/sigNoz.schem
import { initialQueriesMap } from 'constants/queryBuilder';
import type { Query } from 'types/api/queryBuilder/queryBuilderData';
import { getQueryPanelDefinition, getSupportedSignals } from '../capabilities';
import { getQueryPanelDefinition } from '../capabilities';
import { toPanelType } from '../types/panelKind';
import { fromPerses } from '../../queryV5/persesQueryAdapters';
@@ -22,7 +22,7 @@ export function getPanelBuilderQuery(
if (!definition) {
return null;
}
const [defaultSignal] = getSupportedSignals(kind);
const [defaultSignal] = definition.supportedSignals;
// A query-less panel seeds from the kind's first supported signal — `fromPerses`'s
// metrics default isn't authorable in every kind (e.g. List).
if (panel.spec.queries.length === 0 && defaultSignal) {

View File

@@ -1,65 +0,0 @@
import { initialQueryAIWithType } from 'constants/queryBuilder';
import type { Query } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { DataSource } from 'types/common/queryBuilder';
import { AI_QUERY_MODE, type PanelQueryMode } from '../types/queryModes';
/**
* The mode a query is authored in. The AI mode has no `queryType` of its own — it is a
* builder query tagged per-query — so it is read off the queries rather than off the
* envelope around them.
*/
export function getQueryMode(query: Query): PanelQueryMode {
if (
query.queryType === EQueryType.QUERY_BUILDER &&
(query.builder?.queryData ?? []).some(
(q) => q.builderQueryType === AI_QUERY_MODE,
)
) {
return AI_QUERY_MODE;
}
return query.queryType;
}
/**
* Retargets a query at `mode`, which is the only safe way to change it: the AI tag lives on
* every query and has to be stamped or cleared in step with `queryType`, or the tab and the
* queries disagree.
*
* Entering the AI mode keeps queries that are already traces (the common case — the user was
* building a trace query and wants AI's field set); anything else is replaced with a fresh AI
* query rather than coerced, which would leave metric aggregations on a traces query.
*/
export function withQueryMode(query: Query, mode: PanelQueryMode): Query {
if (mode === AI_QUERY_MODE) {
const queryData = query.builder?.queryData ?? [];
const isAllTraces =
queryData.length > 0 &&
queryData.every((q) => q.dataSource === DataSource.TRACES);
return {
...query,
queryType: EQueryType.QUERY_BUILDER,
builder: isAllTraces
? {
...query.builder,
queryData: queryData.map((q) => ({
...q,
builderQueryType: AI_QUERY_MODE,
})),
}
: initialQueryAIWithType.builder,
};
}
return {
...query,
queryType: mode,
builder: {
...query.builder,
queryData: (query.builder?.queryData ?? []).map((q) => ({
...q,
builderQueryType: undefined,
})),
},
};
}

View File

@@ -28,17 +28,12 @@ import { useViewPanel } from '../hooks/useViewPanel';
import { buildMoveItems } from '../utils/buildMoveItems';
import MenuActionItem from '../../../components/MenuActionItem/MenuActionItem';
import type { BrandedPermission } from 'lib/authz/hooks/useAuthZ/types';
import { isAIBuilderEnvelope } from '../../../queryV5/builderEnvelope';
import { toQueryEnvelopes } from '../../../queryV5/buildQueryRangeRequest';
import { useDashboardEditContext } from '../../../hooks/useDashboardEditContext';
// Stable fallback so renders without layout context don't churn the mutation
// hooks' deps (a fresh [] each render would re-create their callbacks).
const EMPTY_SECTIONS: DashboardSection[] = [];
const ALERT_FROM_AI_PANEL_REASON =
'Alerts are not available for AI Query Builder panels';
interface UsePanelActionItemsArgs {
panelId: string;
/** The panel itself — seeds "Create Alerts" and the download filename. */
@@ -68,10 +63,6 @@ export function usePanelActionItems({
panelActions,
}: UsePanelActionItemsArgs): PanelActionItems {
const panelKind = panel.spec.plugin.kind;
// The alert builder has no AI query mode, so the flow would open on an empty query.
const isAIPanel = toQueryEnvelopes(panel.spec.queries).some(
isAIBuilderEnvelope,
);
const { isEditable, editChecks, editDisabledTooltip } =
useDashboardEditContext();
const openPanelEditor = useOpenPanelEditor();
@@ -166,15 +157,7 @@ export function usePanelActionItems({
if (panelCapabilities.createAlert) {
dataGroup.push({
key: 'create-alert',
label: (
<MenuActionItem
label="Create Alerts"
icon={<Bell size={14} />}
checks={[]}
disabledTooltip={isAIPanel ? ALERT_FROM_AI_PANEL_REASON : undefined}
/>
),
disabled: isAIPanel,
label: row('Create Alerts', <Bell size={14} />, { checks: [] }),
onClick: (): void => createAlert(panel, panelId),
});
}
@@ -214,7 +197,6 @@ export function usePanelActionItems({
editChecks,
editDisabledTooltip,
panelCapabilities,
isAIPanel,
panel,
panelActions,
sections,

View File

@@ -60,7 +60,7 @@ function QueryViewModalBody({
setSpec,
panelDefinition,
signal,
queryMode,
queryType,
query,
runQuery,
resetQuery,
@@ -152,7 +152,7 @@ function QueryViewModalBody({
}}
onSwitchToEdit={onSwitchToEdit}
panelKind={draft.spec.plugin.kind}
queryMode={queryMode}
queryType={queryType}
signal={signal}
onChangePanelKind={onChangePanelKind}
onResetQuery={resetQuery}

View File

@@ -9,7 +9,6 @@ import type {
} from 'container/TopNav/DateTimeSelectionV2/types';
import { usePanelTypeSelectItems } from 'pages/DashboardPage/DashboardContainer/PanelEditor/ConfigPane/PanelTypeSwitcher/usePanelTypeSelectItems';
import ConfigSelect from 'pages/DashboardPage/DashboardContainer/PanelEditor/ConfigPane/controls/ConfigSelect/ConfigSelect';
import type { PanelQueryMode } from '../../../Panels/types/queryModes';
import type { PanelKind } from 'pages/DashboardPage/DashboardContainer/Panels/types/panelKind';
import { EQueryType } from 'types/common/dashboard';
@@ -42,7 +41,7 @@ interface QueryViewModalHeaderProps extends ViewPanelModalHeaderBaseProps {
* selector greys out kinds that can't be authored in it — e.g. List is
* Query-Builder-only, so PromQL/ClickHouse disable it.
*/
queryMode: PanelQueryMode;
queryType: EQueryType;
/** Current builder datasource — greys out kinds that don't support it (e.g. List needs logs/traces, not metrics). */
signal: TelemetrytypesSignalDTO;
/** Restore the saved query + kind (drilldown reset). */
@@ -75,7 +74,7 @@ function ViewPanelModalHeader(props: ViewPanelModalHeaderProps): JSX.Element {
// Same capabilities-guarded options as the editor's PanelTypeSwitcher, so the two
// selectors disable the same kinds (e.g. List under PromQL, metrics-only kinds).
const panelTypeItems = usePanelTypeSelectItems({
mode: query?.queryMode ?? EQueryType.QUERY_BUILDER,
queryType: query?.queryType ?? EQueryType.QUERY_BUILDER,
signal: query?.signal,
});

View File

@@ -13,13 +13,12 @@ import type { RenderableQueryPanelDefinition } from 'pages/DashboardPage/Dashboa
import { toPanelType } from 'pages/DashboardPage/DashboardContainer/Panels/types/panelKind';
import { resolveSignal } from 'pages/DashboardPage/DashboardContainer/Panels/utils/getBuilderQueries';
import { buildViewPanelSpec } from 'pages/DashboardPage/DashboardContainer/Panels/utils/drilldown/buildViewPanelSpec';
import { getQueryMode } from 'pages/DashboardPage/DashboardContainer/Panels/utils/queryMode';
import { fromPerses } from 'pages/DashboardPage/DashboardContainer/queryV5/persesQueryAdapters';
import {
type PanelQueryTimeOverride,
type UsePanelQueryResult,
} from 'pages/DashboardPage/DashboardContainer/hooks/usePanelQuery';
import type { PanelQueryMode } from 'pages/DashboardPage/DashboardContainer/Panels/types/queryModes';
import type { EQueryType } from 'types/common/dashboard';
interface UseViewPanelModeArgs {
panel: DashboardtypesPanelDTO;
@@ -44,7 +43,7 @@ export interface UseViewPanelModeReturn {
*/
signal: TelemetrytypesSignalDTO;
/** Active query type (selected builder tab) — drives the panel-type selector's disabled rule. */
queryMode: PanelQueryMode;
queryType: EQueryType;
/** Query result for the draft over the per-view window. */
query: UsePanelQueryResult;
/** Stage & run the live builder query into the draft (drilldown; not persisted). */
@@ -133,7 +132,7 @@ export function useViewPanelMode({
setSpec,
panelDefinition,
signal,
queryMode: getQueryMode(currentQuery),
queryType: currentQuery.queryType,
query,
runQuery,
resetQuery,

View File

@@ -46,10 +46,6 @@ beforeAll(() => {
});
// jest.config maps the real hook to a no-op mock; this suite needs real navigation.
jest.mock('hooks/useIsAIObservabilityEnabled', () => ({
useIsAIObservabilityEnabled: (): boolean => false,
}));
jest.mock('hooks/useSafeNavigate', () =>
jest
.requireActual('tests/browser-history-safe-navigate')

View File

@@ -16,10 +16,6 @@ import ViewPanelModal from '../ViewPanelModal/ViewPanelModal';
import { useViewPanel } from '../hooks/useViewPanel';
// jest.config maps the real hook to a no-op mock; this suite needs real navigation.
jest.mock('hooks/useIsAIObservabilityEnabled', () => ({
useIsAIObservabilityEnabled: (): boolean => false,
}));
jest.mock('hooks/useSafeNavigate', () =>
jest
.requireActual('tests/browser-history-safe-navigate')

View File

@@ -104,23 +104,6 @@ describe('toQueryEnvelopes', () => {
);
});
it('wraps a bare AI builder plugin as a builder_ai_query envelope', () => {
const ai = [
{
kind: 'TimeSeriesQuery',
spec: {
plugin: {
kind: 'signoz/AIBuilderQuery',
spec: { name: 'A', signal: 'traces' },
},
},
},
] as unknown as DashboardtypesQueryDTO[];
expect(toQueryEnvelopes(ai)).toStrictEqual([
{ type: 'builder_ai_query', spec: { name: 'A', signal: 'traces' } },
]);
});
it('wraps PromQL and ClickHouse plugins with their envelope types', () => {
const prom = [
{

View File

@@ -1,58 +0,0 @@
import type { Querybuildertypesv5QueryEnvelopeDTO } from 'api/generated/services/sigNoz.schemas';
import {
isAIBuilderEnvelope,
isBuilderEnvelope,
isBuilderPluginKind,
} from '../builderEnvelope';
// Only `type` is read; the generated envelope union erases spec to unknown anyway.
const envelope = (type: string): Querybuildertypesv5QueryEnvelopeDTO =>
({ type, spec: {} }) as unknown as Querybuildertypesv5QueryEnvelopeDTO;
describe('builder envelope predicates', () => {
describe('isBuilderEnvelope', () => {
it.each(['builder_query', 'builder_ai_query'])(
'accepts %s — both carry a builder query spec',
(type) => {
expect(isBuilderEnvelope(envelope(type))).toBe(true);
},
);
// Formula and TraceOperator carry no signal and reference other queries by name.
it.each([
'builder_formula',
'builder_trace_operator',
'promql',
'clickhouse_sql',
])('rejects %s', (type) => {
expect(isBuilderEnvelope(envelope(type))).toBe(false);
});
});
describe('isAIBuilderEnvelope', () => {
it('accepts builder_ai_query', () => {
expect(isAIBuilderEnvelope(envelope('builder_ai_query'))).toBe(true);
});
it('rejects builder_query, which is the whole point of the narrower check', () => {
expect(isAIBuilderEnvelope(envelope('builder_query'))).toBe(false);
});
});
describe('isBuilderPluginKind', () => {
it.each(['signoz/BuilderQuery', 'signoz/AIBuilderQuery'])(
'accepts %s — both wrap a builder query spec directly',
(kind) => {
expect(isBuilderPluginKind(kind)).toBe(true);
},
);
it.each(['signoz/CompositeQuery', 'signoz/PromQLQuery'])(
'rejects %s',
(kind) => {
expect(isBuilderPluginKind(kind)).toBe(false);
},
);
});
});

View File

@@ -2,11 +2,7 @@ import type {
DashboardtypesQueryDTO,
Querybuildertypesv5QueryEnvelopeDTO,
} from 'api/generated/services/sigNoz.schemas';
import {
initialQueriesMap,
initialQueryAIWithType,
PANEL_TYPES,
} from 'constants/queryBuilder';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import type { Query } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { DataSource } from 'types/common/queryBuilder';
@@ -197,24 +193,6 @@ describe('persesQueryAdapters', () => {
);
});
it('emits a bare signoz/AIBuilderQuery for an AI List panel', () => {
const result = toPerses(initialQueryAIWithType, PANEL_TYPES.LIST);
expect(result).toHaveLength(1);
expect(result[0].spec.plugin.kind).toBe('signoz/AIBuilderQuery');
});
// List rejects CompositeQuery backend-side, so the plugin kind is the only place
// the AI-ness can survive a save.
it('preserves an AI List query through toPerses → fromPerses', () => {
const perses = toPerses(initialQueryAIWithType, PANEL_TYPES.LIST);
const restored = fromPerses(perses, PANEL_TYPES.LIST);
expect(restored.builder.queryData[0].builderQueryType).toBe(
'builder_ai_query',
);
});
it('preserves a List builder query through toPerses → fromPerses', () => {
const original: Query = initialQueriesMap[DataSource.LOGS];

View File

@@ -11,14 +11,12 @@ import type {
} from 'api/generated/services/sigNoz.schemas';
import {
Querybuildertypesv5OrderDirectionDTO,
Querybuildertypesv5QueryEnvelopeBuilderAIDTOType,
Querybuildertypesv5QueryEnvelopeBuilderDTOType,
Querybuildertypesv5QueryEnvelopeClickHouseSQLDTOType,
Querybuildertypesv5QueryEnvelopePromQLDTOType,
} from 'api/generated/services/sigNoz.schemas';
import type { PanelQueryCapabilities } from '../Panels/types/panelCapabilities';
import { isBuilderEnvelope } from './builderEnvelope';
// Narrow view over the envelope spec variants. Orval erases envelope `spec` to `unknown`, so
// shared fields are read through this view with a localized cast at the envelope boundary.
@@ -60,14 +58,6 @@ export function toQueryEnvelopes(
spec: plugin.spec as Querybuildertypesv5BuilderQuerySpecDTO,
},
];
case 'signoz/AIBuilderQuery':
// Same wire shape; the widening is only orval's separate `signal` enum, which TS treats nominally.
return [
{
type: Querybuildertypesv5QueryEnvelopeBuilderAIDTOType.builder_ai_query,
spec: plugin.spec,
} as unknown as Querybuildertypesv5QueryEnvelopeDTO,
];
case 'signoz/PromQLQuery':
return [
{
@@ -135,10 +125,13 @@ function withBarStepInterval(
): Querybuildertypesv5QueryEnvelopeDTO[] {
const stepInterval = getBarStepIntervalSeconds(startMs, endMs);
return envelopes.map((envelope) => {
if (!isBuilderEnvelope(envelope)) {
if (
envelope.type !==
Querybuildertypesv5QueryEnvelopeBuilderDTOType.builder_query
) {
return envelope;
}
if ((envelope.spec as QuerySpecView | undefined)?.stepInterval) {
if (envelope.spec?.stepInterval) {
return envelope;
}
return {
@@ -146,8 +139,8 @@ function withBarStepInterval(
spec: {
...envelope.spec,
stepInterval,
},
} as Querybuildertypesv5QueryEnvelopeDTO;
} as Querybuildertypesv5BuilderQuerySpecDTO,
};
});
}
@@ -160,7 +153,10 @@ function withListOrderTiebreaker(
envelopes: Querybuildertypesv5QueryEnvelopeDTO[],
): Querybuildertypesv5QueryEnvelopeDTO[] {
return envelopes.map((envelope) => {
if (!isBuilderEnvelope(envelope)) {
if (
envelope.type !==
Querybuildertypesv5QueryEnvelopeBuilderDTOType.builder_query
) {
return envelope;
}
const spec = envelope.spec as QuerySpecView;
@@ -185,8 +181,8 @@ function withListOrderTiebreaker(
...primary,
{ key: { name: 'id' }, direction: primary[0].direction },
],
},
} as Querybuildertypesv5QueryEnvelopeDTO;
} as Querybuildertypesv5BuilderQuerySpecDTO,
};
});
}
@@ -199,7 +195,10 @@ function withPagination(
{ offset, limit }: { offset: number; limit: number },
): Querybuildertypesv5QueryEnvelopeDTO[] {
return envelopes.map((envelope) => {
if (!isBuilderEnvelope(envelope)) {
if (
envelope.type !==
Querybuildertypesv5QueryEnvelopeBuilderDTOType.builder_query
) {
return envelope;
}
return {
@@ -208,8 +207,8 @@ function withPagination(
...envelope.spec,
offset,
limit,
},
} as Querybuildertypesv5QueryEnvelopeDTO;
} as Querybuildertypesv5BuilderQuerySpecDTO,
};
});
}
@@ -302,7 +301,11 @@ export function hasRunnableQueries(queries: DashboardtypesQueryDTO[]): boolean {
}
const metricsSpecs = envelopes
.filter(isBuilderEnvelope)
.filter(
(envelope) =>
envelope.type ===
Querybuildertypesv5QueryEnvelopeBuilderDTOType.builder_query,
)
.map((envelope) => envelope.spec as QuerySpecView)
.filter((spec) => spec.signal === 'metrics');

View File

@@ -1,35 +0,0 @@
import type { Querybuildertypesv5QueryEnvelopeDTO } from 'api/generated/services/sigNoz.schemas';
import {
DashboardtypesQueryPluginKindDTO,
Querybuildertypesv5QueryEnvelopeBuilderAIDTOType,
Querybuildertypesv5QueryEnvelopeBuilderDTOType,
} from 'api/generated/services/sigNoz.schemas';
const BUILDER_ENVELOPE_TYPES: string[] = [
Querybuildertypesv5QueryEnvelopeBuilderDTOType.builder_query,
Querybuildertypesv5QueryEnvelopeBuilderAIDTOType.builder_ai_query,
];
export function isBuilderEnvelope(
envelope: Querybuildertypesv5QueryEnvelopeDTO,
): boolean {
return BUILDER_ENVELOPE_TYPES.includes(envelope.type);
}
export function isAIBuilderEnvelope(
envelope: Querybuildertypesv5QueryEnvelopeDTO,
): boolean {
return (
envelope.type ===
Querybuildertypesv5QueryEnvelopeBuilderAIDTOType.builder_ai_query
);
}
const BUILDER_PLUGIN_KINDS: string[] = [
DashboardtypesQueryPluginKindDTO['signoz/BuilderQuery'],
DashboardtypesQueryPluginKindDTO['signoz/AIBuilderQuery'],
];
export function isBuilderPluginKind(kind: string): boolean {
return BUILDER_PLUGIN_KINDS.includes(kind);
}

View File

@@ -1,14 +1,13 @@
import type {
DashboardtypesAIBuilderQuerySpecDTO,
DashboardtypesBuilderQuerySpecDTO,
DashboardtypesQueryDTO,
Querybuildertypesv5CompositeQueryDTO,
Querybuildertypesv5QueryEnvelopeDTO,
} from 'api/generated/services/sigNoz.schemas';
import {
DashboardtypesQueryPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesAIBuilderQuerySpecDTOKind as AIBuilderQueryPluginKind,
DashboardtypesQueryPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesBuilderQuerySpecDTOKind as BuilderQueryPluginKind,
DashboardtypesQueryPluginVariantGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5CompositeQueryDTOKind as CompositeQueryPluginKind,
Querybuildertypesv5QueryEnvelopeBuilderDTOType,
Querybuildertypesv5QueryEnvelopeClickHouseSQLDTOType,
Querybuildertypesv5QueryEnvelopePromQLDTOType,
Querybuildertypesv5RequestTypeDTO,
@@ -22,7 +21,6 @@ import type { QueryEnvelope } from 'types/api/v5/queryRange';
import { EQueryType } from 'types/common/dashboard';
import { DataSource } from 'types/common/queryBuilder';
import { isAIBuilderEnvelope, isBuilderEnvelope } from './builderEnvelope';
import { toQueryEnvelopes } from './buildQueryRangeRequest';
/**
@@ -46,6 +44,11 @@ const toGeneratedEnvelopes = (
): Querybuildertypesv5QueryEnvelopeDTO[] =>
envelopes as unknown as Querybuildertypesv5QueryEnvelopeDTO[];
const isBuilderQueryEnvelope = (
envelope: Querybuildertypesv5QueryEnvelopeDTO,
): boolean =>
envelope.type === Querybuildertypesv5QueryEnvelopeBuilderDTOType.builder_query;
/**
* Clears the V1 explorer's `pageSize`/`offset` before conversion — the shared mapper folds
* `pageSize` into the V5 `limit`, which usePanelQuery would read as a user cap and hide the
@@ -152,9 +155,8 @@ export function fromPerses(
/**
* V1 `Query` → perses panel queries (to write the builder result back to the editor
* draft). Wrapped in a single `signoz/CompositeQuery` to satisfy the
* `panel.queries.length === 1` invariant. Exception: List rejects `signoz/CompositeQuery`
* backend-side, so it emits its one builder query as the bare plugin matching the query's
* own kind — a bare plugin carries no envelope `type`, so the kind is what preserves it.
* `panel.queries.length === 1` invariant. Exception: List emits its one builder query
* as a bare `signoz/BuilderQuery` because the backend rejects a `signoz/CompositeQuery`.
*/
export function toPerses(
query: Query,
@@ -168,24 +170,21 @@ export function toPerses(
const envelopes = toGeneratedEnvelopes(composite.queries ?? []);
if (panelType === PANEL_TYPES.LIST) {
const builder = envelopes.find(isBuilderEnvelope);
const builder = envelopes.find(isBuilderQueryEnvelope);
if (!builder) {
return [];
}
// Envelope `spec` is undiscriminated, so narrow it to the spec its plugin kind declares.
const plugin = isAIBuilderEnvelope(builder)
? {
kind: AIBuilderQueryPluginKind['signoz/AIBuilderQuery'],
spec: builder.spec as unknown as DashboardtypesAIBuilderQuerySpecDTO,
}
: {
kind: BuilderQueryPluginKind['signoz/BuilderQuery'],
spec: builder.spec as DashboardtypesBuilderQuerySpecDTO,
};
return [
{
kind: panelTypeToRequestType(panelType),
spec: { plugin },
spec: {
plugin: {
kind: BuilderQueryPluginKind['signoz/BuilderQuery'],
// The generated envelope union doesn't discriminate `spec` by `type`, so
// narrow the filtered builder query to the dashboard builder spec.
spec: builder.spec as DashboardtypesBuilderQuerySpecDTO,
},
},
},
];
}

View File

@@ -3,9 +3,10 @@ import type {
Querybuildertypesv5QueryRangeRequestDTO,
Querybuildertypesv5ScalarDataDTO,
} from 'api/generated/services/sigNoz.schemas';
import { Querybuildertypesv5QueryEnvelopeClickHouseSQLDTOType } from 'api/generated/services/sigNoz.schemas';
import { isBuilderEnvelope } from './builderEnvelope';
import {
Querybuildertypesv5QueryEnvelopeBuilderDTOType,
Querybuildertypesv5QueryEnvelopeClickHouseSQLDTOType,
} from 'api/generated/services/sigNoz.schemas';
import type { PanelTable, PanelTableColumn } from './types';
@@ -27,12 +28,13 @@ export function extractAggregationsPerQuery(
): AggregationsPerQuery {
const perQuery: AggregationsPerQuery = {};
(requestPayload?.compositeQuery?.queries ?? []).forEach((envelope) => {
if (!isBuilderEnvelope(envelope)) {
if (
envelope.type !==
Querybuildertypesv5QueryEnvelopeBuilderDTOType.builder_query
) {
return;
}
const spec = envelope.spec as
| { name?: string; aggregations?: unknown }
| undefined;
const spec = envelope.spec;
if (spec?.name && spec.aggregations) {
perQuery[spec.name] = spec.aggregations as AggregationView[];
}

View File

@@ -1,7 +1,7 @@
import type { Query } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { isQueryModeSupportedByPanelKind } from '../DashboardContainer/Panels/capabilities';
import { isQueryTypeSupportedByPanelKind } from '../DashboardContainer/Panels/capabilities';
import { getPanelDefinition } from '../DashboardContainer/Panels/registry';
import { SectionKind } from '../DashboardContainer/Panels/types/sections';
import { buildDefaultQueries } from '../DashboardContainer/Panels/utils/buildDefaultQueries';
@@ -22,7 +22,7 @@ jest.mock('../DashboardContainer/Panels/registry', () => ({
getPanelDefinition: jest.fn(),
}));
jest.mock('../DashboardContainer/Panels/capabilities', () => ({
isQueryModeSupportedByPanelKind: jest.fn(),
isQueryTypeSupportedByPanelKind: jest.fn(),
// Real predicate: these specs exercise query kinds; the static guard has its
// own case below.
isStaticPanelKind: jest.requireActual(
@@ -34,7 +34,7 @@ const mockToPerses = toPerses as jest.Mock;
const mockBuildDefaultQueries = buildDefaultQueries as jest.Mock;
const mockBuildPluginSpec = buildPluginSpec as jest.Mock;
const mockGetPanelDefinition = getPanelDefinition as jest.Mock;
const mockIsQueryModeSupported = isQueryModeSupportedByPanelKind as jest.Mock;
const mockIsQueryTypeSupported = isQueryTypeSupportedByPanelKind as jest.Mock;
const DEFAULT_QUERIES = [{ kind: 'default' }];
const CONVERTED_QUERIES = [{ kind: 'converted' }];
@@ -54,7 +54,7 @@ describe('buildNewPanelSeed', () => {
mockBuildDefaultQueries.mockReturnValue(DEFAULT_QUERIES);
mockBuildPluginSpec.mockReturnValue(BASE_SPEC);
mockGetPanelDefinition.mockReturnValue({ sections: withUnit });
mockIsQueryModeSupported.mockReturnValue(true);
mockIsQueryTypeSupported.mockReturnValue(true);
});
it('uses the kind default seed when it is not an explorer export', () => {
@@ -77,7 +77,7 @@ describe('buildNewPanelSeed', () => {
});
it('coerces a builder-only kind to Table for a ClickHouse query', () => {
mockIsQueryModeSupported.mockReturnValue(false);
mockIsQueryTypeSupported.mockReturnValue(false);
mockToPerses.mockReturnValue(CONVERTED_QUERIES);
const seed = buildNewPanelSeed(
@@ -91,7 +91,7 @@ describe('buildNewPanelSeed', () => {
});
it('coerces a builder-only kind to TimeSeries for a PromQL query', () => {
mockIsQueryModeSupported.mockReturnValue(false);
mockIsQueryTypeSupported.mockReturnValue(false);
mockToPerses.mockReturnValue(CONVERTED_QUERIES);
const seed = buildNewPanelSeed(

View File

@@ -4,14 +4,13 @@ import { EQueryType } from 'types/common/dashboard';
import {
isStaticPanelKind,
isQueryModeSupportedByPanelKind,
isQueryTypeSupportedByPanelKind,
} from '../DashboardContainer/Panels/capabilities';
import { getPanelDefinition } from '../DashboardContainer/Panels/registry';
import { toPanelType } from '../DashboardContainer/Panels/types/panelKind';
import type { PanelKind } from '../DashboardContainer/Panels/types/panelKind';
import { SectionKind } from '../DashboardContainer/Panels/types/sections';
import { buildDefaultQueries } from '../DashboardContainer/Panels/utils/buildDefaultQueries';
import { getQueryMode } from '../DashboardContainer/Panels/utils/queryMode';
import {
buildPluginSpec,
type SeededPluginSpec,
@@ -37,9 +36,7 @@ function resolveSeededPanelKind(
requestedKind: PanelKind,
compositeQuery: Query,
): PanelKind {
if (
isQueryModeSupportedByPanelKind(requestedKind, getQueryMode(compositeQuery))
) {
if (isQueryTypeSupportedByPanelKind(requestedKind, compositeQuery.queryType)) {
return requestedKind;
}
return (

View File

@@ -4,14 +4,15 @@ import (
"net/http"
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
citypes "github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/gorilla/mux"
)
func (provider *provider) addCloudIntegrationRoutes(router *mux.Router) error {
if err := router.Handle("/api/v1/cloud_integrations/{cloud_provider}/credentials", handler.New(
provider.authzMiddleware.AdminAccess(provider.cloudIntegrationHandler.GetConnectionCredentials),
provider.authzMiddleware.CheckResources(provider.cloudIntegrationHandler.GetConnectionCredentials, authtypes.SigNozAdminRoleName),
handler.OpenAPIDef{
ID: "GetConnectionCredentials",
Tags: []string{"cloudintegration"},
@@ -24,14 +25,56 @@ func (provider *provider) addCloudIntegrationRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
SecuritySchemes: newScopedSecuritySchemes([]string{
coretypes.ResourceMetaResourceIngestionKey.Scope(coretypes.VerbCreate),
coretypes.ResourceServiceAccount.Scope(coretypes.VerbCreate),
coretypes.ResourceMetaResourceFactorAPIKey.Scope(coretypes.VerbCreate),
coretypes.ResourceServiceAccount.Scope(coretypes.VerbAttach),
coretypes.ResourceRole.Scope(coretypes.VerbAttach),
}),
},
handler.WithResourceDefs(
handler.BasicResourceDef{
Resource: coretypes.ResourceMetaResourceIngestionKey,
Verb: coretypes.VerbCreate,
Category: coretypes.ActionCategoryConfigurationChange,
Selector: coretypes.WildcardSelector,
},
handler.BasicResourceDef{
Resource: coretypes.ResourceServiceAccount,
Verb: coretypes.VerbCreate,
Category: coretypes.ActionCategoryAccessControl,
Selector: coretypes.WildcardSelector,
},
handler.BasicResourceDef{
Resource: coretypes.ResourceMetaResourceFactorAPIKey,
Verb: coretypes.VerbCreate,
Category: coretypes.ActionCategoryAccessControl,
Selector: coretypes.WildcardSelector,
},
handler.AttachDetachSiblingResourceDef{
Verb: coretypes.VerbAttach,
Category: coretypes.ActionCategoryAccessControl,
SourceResource: coretypes.ResourceServiceAccount,
SourceSelector: coretypes.WildcardSelector,
TargetResource: coretypes.ResourceRole,
TargetIDs: signozViewerRoleNamesExtractor(),
TargetSelector: coretypes.IDSelector,
},
handler.AttachDetachParentChildResourceDef{
Verb: coretypes.VerbAttach,
Category: coretypes.ActionCategoryAccessControl,
ParentResource: coretypes.ResourceServiceAccount,
ParentSelector: coretypes.WildcardSelector,
ChildResource: coretypes.ResourceMetaResourceFactorAPIKey,
},
),
)).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/cloud_integrations/{cloud_provider}/accounts", handler.New(
provider.authzMiddleware.AdminAccess(provider.cloudIntegrationHandler.CreateAccount),
provider.authzMiddleware.CheckResources(provider.cloudIntegrationHandler.CreateAccount, authtypes.SigNozAdminRoleName),
handler.OpenAPIDef{
ID: "CreateAccount",
Tags: []string{"cloudintegration"},
@@ -44,14 +87,21 @@ func (provider *provider) addCloudIntegrationRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceCloudIntegration.Scope(coretypes.VerbCreate)}),
},
handler.WithResourceDefs(handler.BasicResourceDef{
Resource: coretypes.ResourceMetaResourceCloudIntegration,
Verb: coretypes.VerbCreate,
Category: coretypes.ActionCategoryConfigurationChange,
ID: coretypes.ResponseJSONPath("data.id"),
Selector: coretypes.WildcardSelector,
}),
)).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/cloud_integrations/{cloud_provider}/accounts", handler.New(
provider.authzMiddleware.AdminAccess(provider.cloudIntegrationHandler.ListAccounts),
provider.authzMiddleware.CheckResources(provider.cloudIntegrationHandler.ListAccounts, authtypes.SigNozAdminRoleName, authtypes.SigNozEditorRoleName, authtypes.SigNozViewerRoleName),
handler.OpenAPIDef{
ID: "ListAccounts",
Tags: []string{"cloudintegration"},
@@ -64,14 +114,20 @@ func (provider *provider) addCloudIntegrationRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceCloudIntegration.Scope(coretypes.VerbList)}),
},
handler.WithResourceDefs(handler.BasicResourceDef{
Resource: coretypes.ResourceMetaResourceCloudIntegration,
Verb: coretypes.VerbList,
Category: coretypes.ActionCategoryDataAccess,
Selector: coretypes.WildcardSelector,
}),
)).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/cloud_integrations/{cloud_provider}/accounts/{id}", handler.New(
provider.authzMiddleware.AdminAccess(provider.cloudIntegrationHandler.GetAccount),
provider.authzMiddleware.CheckResources(provider.cloudIntegrationHandler.GetAccount, authtypes.SigNozAdminRoleName, authtypes.SigNozEditorRoleName, authtypes.SigNozViewerRoleName),
handler.OpenAPIDef{
ID: "GetAccount",
Tags: []string{"cloudintegration"},
@@ -84,14 +140,21 @@ func (provider *provider) addCloudIntegrationRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceCloudIntegration.Scope(coretypes.VerbRead)}),
},
handler.WithResourceDefs(handler.BasicResourceDef{
Resource: coretypes.ResourceMetaResourceCloudIntegration,
Verb: coretypes.VerbRead,
Category: coretypes.ActionCategoryDataAccess,
ID: coretypes.PathParam("id"),
Selector: coretypes.IDSelector,
}),
)).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/cloud_integrations/{cloud_provider}/accounts/{id}", handler.New(
provider.authzMiddleware.AdminAccess(provider.cloudIntegrationHandler.UpdateAccount),
provider.authzMiddleware.CheckResources(provider.cloudIntegrationHandler.UpdateAccount, authtypes.SigNozAdminRoleName),
handler.OpenAPIDef{
ID: "UpdateAccount",
Tags: []string{"cloudintegration"},
@@ -104,14 +167,21 @@ func (provider *provider) addCloudIntegrationRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceCloudIntegration.Scope(coretypes.VerbUpdate)}),
},
handler.WithResourceDefs(handler.BasicResourceDef{
Resource: coretypes.ResourceMetaResourceCloudIntegration,
Verb: coretypes.VerbUpdate,
Category: coretypes.ActionCategoryConfigurationChange,
ID: coretypes.PathParam("id"),
Selector: coretypes.IDSelector,
}),
)).Methods(http.MethodPut).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/cloud_integrations/{cloud_provider}/accounts/{id}", handler.New(
provider.authzMiddleware.AdminAccess(provider.cloudIntegrationHandler.DisconnectAccount),
provider.authzMiddleware.CheckResources(provider.cloudIntegrationHandler.DisconnectAccount, authtypes.SigNozAdminRoleName),
handler.OpenAPIDef{
ID: "DisconnectAccount",
Tags: []string{"cloudintegration"},
@@ -124,14 +194,21 @@ func (provider *provider) addCloudIntegrationRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceCloudIntegration.Scope(coretypes.VerbDelete)}),
},
handler.WithResourceDefs(handler.BasicResourceDef{
Resource: coretypes.ResourceMetaResourceCloudIntegration,
Verb: coretypes.VerbDelete,
Category: coretypes.ActionCategoryConfigurationChange,
ID: coretypes.PathParam("id"),
Selector: coretypes.IDSelector,
}),
)).Methods(http.MethodDelete).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/cloud_integrations/{cloud_provider}/services", handler.New(
provider.authzMiddleware.AdminAccess(provider.cloudIntegrationHandler.ListServicesMetadata),
provider.authzMiddleware.OpenAccess(provider.cloudIntegrationHandler.ListServicesMetadata),
handler.OpenAPIDef{
ID: "ListServicesMetadata",
Tags: []string{"cloudintegration"},
@@ -144,14 +221,14 @@ func (provider *provider) addCloudIntegrationRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
SecuritySchemes: newScopedSecuritySchemes(nil),
},
)).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/cloud_integrations/{cloud_provider}/accounts/{id}/services", handler.New(
provider.authzMiddleware.AdminAccess(provider.cloudIntegrationHandler.ListAccountServicesMetadata),
provider.authzMiddleware.CheckResources(provider.cloudIntegrationHandler.ListAccountServicesMetadata, authtypes.SigNozAdminRoleName, authtypes.SigNozEditorRoleName, authtypes.SigNozViewerRoleName),
handler.OpenAPIDef{
ID: "ListAccountServicesMetadata",
Tags: []string{"cloudintegration"},
@@ -164,14 +241,21 @@ func (provider *provider) addCloudIntegrationRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceCloudIntegration.Scope(coretypes.VerbList)}),
},
handler.WithResourceDefs(handler.BasicResourceDef{
Resource: coretypes.ResourceMetaResourceCloudIntegration,
Verb: coretypes.VerbRead,
Category: coretypes.ActionCategoryDataAccess,
ID: coretypes.PathParam("id"),
Selector: coretypes.IDSelector,
}),
)).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/cloud_integrations/{cloud_provider}/services/{service_id}", handler.New(
provider.authzMiddleware.AdminAccess(provider.cloudIntegrationHandler.GetService),
provider.authzMiddleware.OpenAccess(provider.cloudIntegrationHandler.GetService),
handler.OpenAPIDef{
ID: "GetService",
Tags: []string{"cloudintegration"},
@@ -184,14 +268,14 @@ func (provider *provider) addCloudIntegrationRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
SecuritySchemes: newScopedSecuritySchemes(nil),
},
)).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/cloud_integrations/{cloud_provider}/accounts/{id}/services/{service_id}", handler.New(
provider.authzMiddleware.AdminAccess(provider.cloudIntegrationHandler.UpdateService),
provider.authzMiddleware.CheckResources(provider.cloudIntegrationHandler.UpdateService, authtypes.SigNozAdminRoleName),
handler.OpenAPIDef{
ID: "UpdateService",
Tags: []string{"cloudintegration"},
@@ -204,14 +288,21 @@ func (provider *provider) addCloudIntegrationRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceCloudIntegration.Scope(coretypes.VerbUpdate)}),
},
handler.WithResourceDefs(handler.BasicResourceDef{
Resource: coretypes.ResourceMetaResourceCloudIntegration,
Verb: coretypes.VerbUpdate,
Category: coretypes.ActionCategoryConfigurationChange,
ID: coretypes.PathParam("id"),
Selector: coretypes.IDSelector,
}),
)).Methods(http.MethodPut).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/cloud_integrations/{cloud_provider}/accounts/{id}/services/{service_id}", handler.New(
provider.authzMiddleware.AdminAccess(provider.cloudIntegrationHandler.GetAccountService),
provider.authzMiddleware.CheckResources(provider.cloudIntegrationHandler.GetAccountService, authtypes.SigNozAdminRoleName, authtypes.SigNozEditorRoleName, authtypes.SigNozViewerRoleName),
handler.OpenAPIDef{
ID: "GetAccountService",
Tags: []string{"cloudintegration"},
@@ -224,8 +315,15 @@ func (provider *provider) addCloudIntegrationRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceCloudIntegration.Scope(coretypes.VerbRead)}),
},
handler.WithResourceDefs(handler.BasicResourceDef{
Resource: coretypes.ResourceMetaResourceCloudIntegration,
Verb: coretypes.VerbRead,
Category: coretypes.ActionCategoryDataAccess,
ID: coretypes.PathParam("id"),
Selector: coretypes.IDSelector,
}),
)).Methods(http.MethodGet).GetError(); err != nil {
return err
}
@@ -233,7 +331,7 @@ func (provider *provider) addCloudIntegrationRoutes(router *mux.Router) error {
// Agent check-in endpoint is kept same as older one to maintain backward compatibility with already deployed agents.
// In the future, this endpoint will be deprecated and a new endpoint will be introduced for consistency with above endpoints.
if err := router.Handle("/api/v1/cloud-integrations/{cloud_provider}/agent-check-in", handler.New(
provider.authzMiddleware.ViewAccess(provider.cloudIntegrationHandler.AgentCheckIn),
provider.authzMiddleware.CheckResources(provider.cloudIntegrationHandler.AgentCheckIn, authtypes.SigNozAdminRoleName, authtypes.SigNozEditorRoleName, authtypes.SigNozViewerRoleName),
handler.OpenAPIDef{
ID: "AgentCheckInDeprecated",
Tags: []string{"cloudintegration"},
@@ -245,15 +343,22 @@ func (provider *provider) addCloudIntegrationRoutes(router *mux.Router) error {
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: true, // this endpoint will be deprecated in future
SecuritySchemes: newSecuritySchemes(types.RoleViewer), // agent role is viewer
Deprecated: true, // this endpoint will be deprecated in future
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceCloudIntegration.Scope(coretypes.VerbRead)}),
},
handler.WithResourceDefs(handler.BasicResourceDef{
Resource: coretypes.ResourceMetaResourceCloudIntegration,
Verb: coretypes.VerbRead,
Category: coretypes.ActionCategoryDataAccess,
ID: coretypes.BodyJSONPath("account_id"),
Selector: coretypes.IDSelector,
}),
)).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/cloud_integrations/{cloud_provider}/accounts/check_in", handler.New(
provider.authzMiddleware.ViewAccess(provider.cloudIntegrationHandler.AgentCheckIn),
provider.authzMiddleware.CheckResources(provider.cloudIntegrationHandler.AgentCheckIn, authtypes.SigNozAdminRoleName, authtypes.SigNozEditorRoleName, authtypes.SigNozViewerRoleName),
handler.OpenAPIDef{
ID: "AgentCheckIn",
Tags: []string{"cloudintegration"},
@@ -266,11 +371,24 @@ func (provider *provider) addCloudIntegrationRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleViewer), // agent role is viewer
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceCloudIntegration.Scope(coretypes.VerbRead)}),
},
handler.WithResourceDefs(handler.BasicResourceDef{
Resource: coretypes.ResourceMetaResourceCloudIntegration,
Verb: coretypes.VerbRead,
Category: coretypes.ActionCategoryDataAccess,
ID: coretypes.BodyJSONPath("cloudIntegrationId"),
Selector: coretypes.IDSelector,
}),
)).Methods(http.MethodPost).GetError(); err != nil {
return err
}
return nil
}
func signozViewerRoleNamesExtractor() coretypes.ResourceIDsExtractor {
return coretypes.ResourceIDsExtractor{Phase: coretypes.PhaseRequest, Fn: func(coretypes.ExtractorContext) ([]string, error) {
return []string{authtypes.SigNozViewerRoleName}, nil
}}
}

View File

@@ -495,8 +495,8 @@ WITH
toDateTime64(%[3]d/1e9, 9) AS start_ts,
toDateTime64(%[4]d/1e9, 9) AS end_ts,
('%[5]s','%[6]s') AS step1,
('%[7]s','%[8]s') AS step2
(%[5]s,%[6]s) AS step1,
(%[7]s,%[8]s) AS step2
SELECT
trace_id,
@@ -527,10 +527,10 @@ LIMIT 5;
containsErrorT2,
startTs,
endTs,
serviceNameT1,
spanNameT1,
serviceNameT2,
spanNameT2,
clickhousesql.StringLiteral(serviceNameT1),
clickhousesql.StringLiteral(spanNameT1),
clickhousesql.StringLiteral(serviceNameT2),
clickhousesql.StringLiteral(spanNameT2),
clauseStep1,
clauseStep2,
t1TimeExpr,
@@ -571,8 +571,8 @@ WITH
toDateTime64(%[3]d/1e9, 9) AS start_ts,
toDateTime64(%[4]d/1e9, 9) AS end_ts,
('%[5]s','%[6]s') AS step1,
('%[7]s','%[8]s') AS step2
(%[5]s,%[6]s) AS step1,
(%[7]s,%[8]s) AS step2
SELECT
trace_id,
@@ -607,10 +607,10 @@ LIMIT 5;
containsErrorT2,
startTs,
endTs,
serviceNameT1,
spanNameT1,
serviceNameT2,
spanNameT2,
clickhousesql.StringLiteral(serviceNameT1),
clickhousesql.StringLiteral(spanNameT1),
clickhousesql.StringLiteral(serviceNameT2),
clickhousesql.StringLiteral(spanNameT2),
clauseStep1,
clauseStep2,
t1TimeExpr,

View File

@@ -25,8 +25,9 @@ const (
// ResolveLogicalFields picks which logical fields a filter term builds conditions
// for. With 0 or 1 field it returns the input unchanged and no warning. When a
// name is ambiguous (several logical fields — a family is one field and never
// ambiguous with itself) it returns a warning; a resource+attribute mix defaults
// to the resource fields (the common intent), noted in the warning.
// ambiguous with itself) it returns a warning; a resource + other-context mix
// (attribute, body, scope, …) defaults to the resource fields (the common
// intent), noted in the warning.
func ResolveLogicalFields(field *telemetrytypes.TelemetryFieldKey, logicalFields []*telemetrytypes.LogicalField) ([]*telemetrytypes.LogicalField, string) {
if len(logicalFields) <= 1 {
return logicalFields, ""
@@ -39,18 +40,17 @@ func ResolveLogicalFields(field *telemetrytypes.TelemetryFieldKey, logicalFields
logicalFields,
)
hasResource, hasAttribute := false, false
hasResource, hasOther := false, false
for _, item := range logicalFields {
switch item.FieldContext {
case telemetrytypes.FieldContextResource:
if item.FieldContext == telemetrytypes.FieldContextResource {
hasResource = true
case telemetrytypes.FieldContextAttribute:
hasAttribute = true
} else {
hasOther = true
}
}
// when there is both resource and attribute context, default to resource only
if hasResource && hasAttribute {
// with resource and any other context, default to resource only
if hasResource && hasOther {
filtered := make([]*telemetrytypes.LogicalField, 0, len(logicalFields))
for _, item := range logicalFields {
if item.FieldContext == telemetrytypes.FieldContextResource {
@@ -58,8 +58,8 @@ func ResolveLogicalFields(field *telemetrytypes.TelemetryFieldKey, logicalFields
}
}
logicalFields = filtered
warning += " " + "Using `resource` context by default. To query attributes explicitly, " +
fmt.Sprintf("use the fully qualified name (e.g., 'attribute.%s')", field.Name)
warning += " " + "Using `resource` context by default. To query another context explicitly, " +
fmt.Sprintf("use the fully qualified name (e.g., 'attribute.%s' or 'body.%s')", field.Name, field.Name)
}
return logicalFields, warning

View File

@@ -175,6 +175,42 @@ func TestResolveLogicalFieldsKeepsFamilyThroughAmbiguity(t *testing.T) {
assert.Equal(t, []string{"deployment.environment.name", "deployment.environment"}, memberNames(resolved[0]))
}
// Resource wins over every other context, not just attribute: a bare key that
// also lives in body or scope must collapse to resource alone, so the surviving
// candidate does not AND against the resource fingerprint CTE.
func TestResolveLogicalFieldsResourceWinsOverOtherContexts(t *testing.T) {
testCases := []struct {
name string
other telemetrytypes.FieldContext
}{
{name: "ResourceOverBody", other: telemetrytypes.FieldContextBody},
{name: "ResourceOverScope", other: telemetrytypes.FieldContextScope},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
requested := &telemetrytypes.TelemetryFieldKey{Name: "service.name"}
fields := []*telemetrytypes.LogicalField{
telemetrytypes.SingleLogicalField("service.name", &telemetrytypes.TelemetryFieldKey{
Name: "service.name",
FieldContext: telemetrytypes.FieldContextResource,
FieldDataType: telemetrytypes.FieldDataTypeString,
}),
telemetrytypes.SingleLogicalField("service.name", &telemetrytypes.TelemetryFieldKey{
Name: "service.name",
FieldContext: testCase.other,
FieldDataType: telemetrytypes.FieldDataTypeString,
}),
}
resolved, warning := ResolveLogicalFields(requested, fields)
assert.NotEmpty(t, warning)
require.Len(t, resolved, 1)
assert.Equal(t, telemetrytypes.FieldContextResource, resolved[0].FieldContext)
})
}
}
// Members of a family with different data types never merge: the identity
// (signal, context, data type) separates them into distinct logical fields.
func TestMatchingLogicalFieldsNeverMergesAcrossDataTypes(t *testing.T) {

View File

@@ -254,6 +254,7 @@ func NewSQLMigrationProviderFactories(
sqlmigration.NewAddIngestionTuplesFactory(sqlstore),
sqlmigration.NewAddSubscriptionTuplesFactory(sqlstore),
sqlmigration.NewNormalizeQuickFilterFieldsFactory(sqlstore),
sqlmigration.NewAddCloudIntegrationTuplesFactory(sqlstore),
)
}

View File

@@ -0,0 +1,166 @@
package sqlmigration
import (
"context"
"database/sql"
"encoding/json"
"time"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/oklog/ulid/v2"
"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect"
"github.com/uptrace/bun/migrate"
)
type addCloudIntegrationTuples struct {
sqlstore sqlstore.SQLStore
}
func NewAddCloudIntegrationTuplesFactory(sqlstore sqlstore.SQLStore) factory.ProviderFactory[SQLMigration, Config] {
return factory.NewProviderFactory(factory.MustNewName("add_cloud_integration_tuples"), func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
return &addCloudIntegrationTuples{sqlstore: sqlstore}, nil
})
}
func (migration *addCloudIntegrationTuples) Register(migrations *migrate.Migrations) error {
return migrations.Register(migration.Up, migration.Down)
}
func (migration *addCloudIntegrationTuples) Up(ctx context.Context, db *bun.DB) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() { _ = tx.Rollback() }()
var storeID string
err = tx.QueryRowContext(ctx, `SELECT id FROM store WHERE name = ? LIMIT 1`, "signoz").Scan(&storeID)
if err != nil {
return err
}
var orgIDs []string
err = tx.NewSelect().
Table("organizations").
Column("id").
Scan(ctx, &orgIDs)
if err != nil && err != sql.ErrNoRows {
return err
}
isPG := migration.sqlstore.BunDB().Dialect().Name() == dialect.PG
// cloud-integration moved from legacy role gates to CheckResources. Existing
// organizations need the same tuples that new organizations receive from the
// managed-role registry at bootstrap.
tuples := []migrationTuple{
{authtypes.SigNozAdminRoleName, "metaresource", "cloud-integration", "create"},
{authtypes.SigNozAdminRoleName, "metaresource", "cloud-integration", "read"},
{authtypes.SigNozAdminRoleName, "metaresource", "cloud-integration", "update"},
{authtypes.SigNozAdminRoleName, "metaresource", "cloud-integration", "delete"},
{authtypes.SigNozAdminRoleName, "metaresource", "cloud-integration", "list"},
{authtypes.SigNozEditorRoleName, "metaresource", "cloud-integration", "read"},
{authtypes.SigNozEditorRoleName, "metaresource", "cloud-integration", "list"},
{authtypes.SigNozViewerRoleName, "metaresource", "cloud-integration", "read"},
{authtypes.SigNozViewerRoleName, "metaresource", "cloud-integration", "list"},
}
for _, orgID := range orgIDs {
for _, tuple := range tuples {
entropy := ulid.DefaultEntropy()
now := time.Now().UTC()
tupleID := ulid.MustNew(ulid.Timestamp(now), entropy).String()
objectID := "organization/" + orgID + "/" + tuple.objectName + "/*"
roleSubject := "organization/" + orgID + "/role/" + tuple.roleName
if isPG {
user := "role:" + roleSubject + "#assignee"
result, err := tx.ExecContext(ctx, `
INSERT INTO tuple (store, object_type, object_id, relation, _user, user_type, ulid, inserted_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (store, object_type, object_id, relation, _user) DO NOTHING`,
storeID, tuple.objectType, objectID, tuple.relation, user, "userset", tupleID, now,
)
if err != nil {
return err
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return err
}
if rowsAffected == 0 {
continue
}
_, err = tx.ExecContext(ctx, `
INSERT INTO changelog (store, object_type, object_id, relation, _user, operation, ulid, inserted_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (store, ulid, object_type) DO NOTHING`,
storeID, tuple.objectType, objectID, tuple.relation, user, 0, tupleID, now,
)
if err != nil {
return err
}
} else {
result, err := tx.ExecContext(ctx, `
INSERT INTO tuple (store, object_type, object_id, relation, user_object_type, user_object_id, user_relation, user_type, ulid, inserted_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (store, object_type, object_id, relation, user_object_type, user_object_id, user_relation) DO NOTHING`,
storeID, tuple.objectType, objectID, tuple.relation, "role", roleSubject, "assignee", "userset", tupleID, now,
)
if err != nil {
return err
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return err
}
if rowsAffected == 0 {
continue
}
_, err = tx.ExecContext(ctx, `
INSERT INTO changelog (store, object_type, object_id, relation, user_object_type, user_object_id, user_relation, operation, ulid, inserted_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (store, ulid, object_type) DO NOTHING`,
storeID, tuple.objectType, objectID, tuple.relation, "role", roleSubject, "assignee", 0, tupleID, now,
)
if err != nil {
return err
}
}
}
}
managedRoleGroups := make(map[string]string, len(coretypes.ManagedRoleToTransactions))
for roleName, transactions := range coretypes.ManagedRoleToTransactions {
data, err := json.Marshal(authtypes.NewTransactionGroupsFromTransactions(transactions))
if err != nil {
return err
}
managedRoleGroups[roleName] = string(data)
}
for _, orgID := range orgIDs {
for roleName, data := range managedRoleGroups {
if _, err := tx.NewUpdate().
Model(new(roles)).
Set("transaction_groups = ?", data).
Where("org_id = ?", orgID).
Where("type = ?", authtypes.RoleTypeManaged.StringValue()).
Where("name = ?", roleName).
Exec(ctx); err != nil {
return err
}
}
}
return tx.Commit()
}
func (migration *addCloudIntegrationTuples) Down(context.Context, *bun.DB) error {
return nil
}

View File

@@ -0,0 +1,90 @@
package logsstatementbuilder
import (
"context"
"testing"
"github.com/SigNoz/signoz/pkg/flagger/flaggertest"
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
"github.com/SigNoz/signoz/pkg/querybuilder"
"github.com/SigNoz/signoz/pkg/statementbuilder"
"github.com/SigNoz/signoz/pkg/telemetryschema/logstelemetryschema"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes/telemetrytypestest"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/stretchr/testify/require"
)
// A key present in both resource and body contexts must filter on resource only.
// The resource condition builds the fingerprint CTE, so a surviving body condition
// would AND against it and match almost nothing (engineering-pod#6086).
func TestStatementBuilderResourceBodyConflict(t *testing.T) {
store := telemetrytypestest.NewMockMetadataStore()
store.SetStaticFields(logstelemetryschema.IntrinsicFields)
store.SetKey(&telemetrytypes.TelemetryFieldKey{
Name: "service.name",
Signal: telemetrytypes.SignalLogs,
FieldContext: telemetrytypes.FieldContextResource,
FieldDataType: telemetrytypes.FieldDataTypeString,
})
bodyKey := &telemetrytypes.TelemetryFieldKey{
Name: "service.name",
Signal: telemetrytypes.SignalLogs,
FieldContext: telemetrytypes.FieldContextBody,
FieldDataType: telemetrytypes.FieldDataTypeString,
}
require.NoError(t, bodyKey.SetJSONAccessPlan(telemetrytypes.JSONColumnMetadata{
BaseColumn: logstelemetryschema.LogsV2BodyV2Column,
PromotedColumn: logstelemetryschema.LogsV2BodyPromotedColumn,
}, map[string][]telemetrytypes.FieldDataType{"service.name": {telemetrytypes.FieldDataTypeString}}))
store.SetKey(bodyKey)
fl := flaggertest.WithUseJSONBody(t, true)
storage := logstelemetryschema.NewStorage()
aggExprRewriter := querybuilder.NewAggExprRewriter(instrumentationtest.New().ToProviderSettings(), nil, storage, fl, telemetrytypes.SignalLogs)
statementBuilder := NewLogQueryStatementBuilder(
instrumentationtest.New().ToProviderSettings(),
store,
storage,
aggExprRewriter,
logstelemetryschema.DefaultFullTextColumn,
fl,
nil,
statementbuilder.Config{SkipResourceFingerprint: statementbuilder.SkipResourceFingerprint{Enabled: false, Threshold: 100000}},
)
testCases := []struct {
name string
requestType qbtypes.RequestType
query qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]
expected qbtypes.Statement
}{
{
name: "AmbiguousKeyFiltersResourceOnly",
requestType: qbtypes.RequestTypeRaw,
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
Signal: telemetrytypes.SignalLogs,
Filter: &qbtypes.Filter{Expression: "service.name = 'webapp'"},
Limit: 10,
},
expected: qbtypes.Statement{
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
Args: []any{"webapp", "%service.name%", "%service.name\":\"webapp%", uint64(1747945619), uint64(1747983448), "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
Warnings: []string{
"Key `service.name` is ambiguous, found 2 different combinations of field context / data type: [name=service.name,context=resource,datatype=string name=service.name,context=body,datatype=string]. Using `resource` context by default. To query another context explicitly, use the fully qualified name (e.g., 'attribute.service.name' or 'body.service.name')",
},
},
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
q, err := statementBuilder.Build(context.Background(), valuer.UUID{}, 1747947419000, 1747983448000, testCase.requestType, testCase.query, nil)
require.NoError(t, err)
require.Equal(t, testCase.expected.Query, q.Query)
require.Equal(t, testCase.expected.Args, q.Args)
require.Equal(t, testCase.expected.Warnings, q.Warnings)
})
}
}

View File

@@ -46,7 +46,6 @@ type QuerySettings struct {
MaxBytesToRead int `mapstructure:"max_bytes_to_read"`
MaxResultRows int `mapstructure:"max_result_rows"`
IgnoreDataSkippingIndices string `mapstructure:"ignore_data_skipping_indices"`
SecondaryIndicesEnableBulkFiltering bool `mapstructure:"secondary_indices_enable_bulk_filtering"`
}
func NewConfigFactory() factory.ConfigFactory {

View File

@@ -72,10 +72,6 @@ func (h *provider) BeforeQuery(ctx context.Context, _ *telemetrystore.QueryEvent
settings["result_overflow_mode"] = ctx.Value("result_overflow_mode")
}
// TODO(srikanthccv): enable it when the "Cannot read all data" issue is fixed
// https://github.com/ClickHouse/ClickHouse/issues/82283
settings["secondary_indices_enable_bulk_filtering"] = false
ctx = clickhouse.Context(ctx, clickhouse.WithSettings(settings))
return ctx
}

View File

@@ -1014,7 +1014,7 @@ func rejectHTTPBasicAuthBeyondPassword(channelName string, httpConfig *commoncfg
basicAuth := httpConfig.BasicAuth
if *basicAuth != (commoncfg.BasicAuth{Username: basicAuth.Username, Password: basicAuth.Password}) {
return errors.NewInvalidInputf(ErrCodeAlertmanagerChannelInvalid, "channel %q sets http_config.basic_auth, which is not supported", channelName)
return errors.NewInvalidInputf(ErrCodeAlertmanagerChannelInvalid, "channel %q sets http_config.basic_auth with fields other than username and password, which is not supported", channelName)
}
return nil
@@ -1026,8 +1026,8 @@ func rejectHTTPAuthorizationBeyondBearer(channelName string, httpConfig *commonc
}
authorization := httpConfig.Authorization
if *authorization != (commoncfg.Authorization{Type: bearerAuthorizationType, Credentials: authorization.Credentials}) {
return errors.NewInvalidInputf(ErrCodeAlertmanagerChannelInvalid, "channel %q sets http_config.authorization, which is not supported", channelName)
if !strings.EqualFold(authorization.Type, bearerAuthorizationType) || *authorization != (commoncfg.Authorization{Type: authorization.Type, Credentials: authorization.Credentials}) {
return errors.NewInvalidInputf(ErrCodeAlertmanagerChannelInvalid, "channel %q sets http_config.authorization with fields other than a bearer token, which is not supported", channelName)
}
return nil

View File

@@ -542,3 +542,42 @@ func TestChannelToPostableChannelRejectsUnrepresentableChannels(t *testing.T) {
})
}
}
// The HTTP auth scheme is case-insensitive (RFC 7235) and Alertmanager sends
// the stored spelling verbatim, so a hand-written receiver may carry any casing.
func TestChannelToPostableChannelReadsWebhookBearerSchemeCaseInsensitively(t *testing.T) {
sendResolved := config.DefaultWebhookConfig.VSendResolved
testCases := []struct {
name string
storedChannelData string
expectedWebhookSpec *ChannelWebhookConfig
}{
{
name: "CanonicalBearer",
storedChannelData: `{"name":"hook","webhook_configs":[{"send_resolved":true,"url":"https://a","http_config":{"authorization":{"type":"Bearer","credentials":"tok"},"follow_redirects":true,"enable_http2":true}}]}`,
expectedWebhookSpec: &ChannelWebhookConfig{SendResolved: &sendResolved, URL: "https://a", BearerToken: "tok"},
},
{
name: "LowercaseBearer",
storedChannelData: `{"name":"hook","webhook_configs":[{"send_resolved":true,"url":"https://b","http_config":{"authorization":{"type":"bearer","credentials":"lower"},"follow_redirects":true,"enable_http2":true}}]}`,
expectedWebhookSpec: &ChannelWebhookConfig{SendResolved: &sendResolved, URL: "https://b", BearerToken: "lower"},
},
{
name: "UppercaseBearer",
storedChannelData: `{"name":"hook","webhook_configs":[{"send_resolved":true,"url":"https://c","http_config":{"authorization":{"type":"BEARER","credentials":"upper"},"follow_redirects":true,"enable_http2":true}}]}`,
expectedWebhookSpec: &ChannelWebhookConfig{SendResolved: &sendResolved, URL: "https://c", BearerToken: "upper"},
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
channel := Channel{DisplayName: "hook", Data: testCase.storedChannelData}
postable, err := channel.toPostableNotificationChannel()
require.NoError(t, err)
assert.Equal(t, ChannelKindWebhook, postable.Config.Kind)
assert.Equal(t, testCase.expectedWebhookSpec, postable.Config.Spec)
})
}
}

View File

@@ -46,6 +46,10 @@ func (extractor ResourceIDsExtractor) IsPhase(phase ExtractPhase) bool {
// OneID lifts a single-id extractor into a one-element ids extractor.
func OneID(extractor ResourceIDExtractor) ResourceIDsExtractor {
if extractor.Fn == nil {
return ResourceIDsExtractor{}
}
return ResourceIDsExtractor{Phase: extractor.Phase, Fn: func(ec ExtractorContext) ([]string, error) {
id, err := extractor.Fn(ec)
if err != nil || id == "" {

View File

@@ -35,7 +35,7 @@ var ManagedRoleToTransactions = map[string][]Transaction{
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindAuthDomain}, WildCardSelectorString)},
{Verb: VerbAttach, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindAuthDomain}, WildCardSelectorString)},
{Verb: VerbDetach, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindAuthDomain}, WildCardSelectorString)},
// cloud-integration — admin only
// cloud-integration — admin can fully manage accounts
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindCloudIntegration}, WildCardSelectorString)},
{Verb: VerbUpdate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindCloudIntegration}, WildCardSelectorString)},
{Verb: VerbDelete, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindCloudIntegration}, WildCardSelectorString)},
@@ -216,6 +216,9 @@ var ManagedRoleToTransactions = map[string][]Transaction{
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindTracesField}, WildCardSelectorString)},
},
SigNozEditorRoleName: {
// cloud-integration — editor can read accounts
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindCloudIntegration}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindCloudIntegration}, WildCardSelectorString)},
// dashboard — full CRUD
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindDashboard}, WildCardSelectorString)},
{Verb: VerbUpdate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindDashboard}, WildCardSelectorString)},
@@ -308,6 +311,9 @@ var ManagedRoleToTransactions = map[string][]Transaction{
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindTracesField}, WildCardSelectorString)},
},
SigNozViewerRoleName: {
// cloud-integration — viewer can read accounts
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindCloudIntegration}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindCloudIntegration}, WildCardSelectorString)},
// dashboard — read only
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindDashboard}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindDashboard}, WildCardSelectorString)},

View File

@@ -52,7 +52,7 @@ var (
ResourceMetaResourceApdexSetting = NewResourceMetaResource(KindApdexSetting)
ResourceMetaResourceAuthDomain = NewResourceMetaResource(KindAuthDomain)
ResourceMetaResourceSession = NewResourceMetaResource(KindSession)
ResourceMetaResourceCloudIntegration = NewResourceMetaResource(KindCloudIntegration)
ResourceMetaResourceCloudIntegration = NewResourceMetaResource(KindCloudIntegration, VerbCreate, VerbList, VerbRead, VerbUpdate, VerbDelete)
ResourceMetaResourceCloudIntegrationService = NewResourceMetaResource(KindCloudIntegrationService)
ResourceMetaResourceIntegration = NewResourceMetaResource(KindIntegration)
ResourceMetaResourceDashboard = NewResourceMetaResource(KindDashboard, VerbCreate, VerbList, VerbRead, VerbUpdate, VerbDelete)

View File

@@ -157,7 +157,6 @@ func (d *DashboardSpec) validateQuery(qi int, q Query, panelKind PanelPluginKind
func validateQueryAllowedForPanel(plugin QueryPlugin, allowed []QueryPluginKind, panelKind PanelPluginKind, path string) error {
compositeSubQueryTypeToPluginKind := map[qb.QueryType]QueryPluginKind{
qb.QueryTypeBuilder: QueryKindBuilder,
qb.QueryTypeBuilderAI: QueryKindAIBuilder,
qb.QueryTypeFormula: QueryKindFormula,
qb.QueryTypeTraceOperator: QueryKindTraceOperator,
qb.QueryTypePromQL: QueryKindPromQL,

View File

@@ -117,22 +117,6 @@ func TestNewStatsFromStorableDashboardsCountsCompositeSubQueries(t *testing.T) {
assert.Equal(t, int64(1), stats[statKeyPanelLogsCount])
}
// An AI builder query is always a traces query, so it counts towards traces.
func TestNewStatsFromStorableDashboardsCountsAIBuilderQueries(t *testing.T) {
aiBuilder := `{
"kind": "time_series",
"spec": {"plugin": {"kind": "signoz/AIBuilderQuery", "spec": {"name": "A", "aggregations": [{"expression": "count()"}]}}}
}`
dashboard := newStatsStorableV2(t, `"p1": `+statsPanel(aiBuilder))
stats := NewStatsFromStorableDashboards([]*StorableDashboard{dashboard})
assert.Equal(t, int64(1), stats[statKeyPanelCount])
assert.Equal(t, int64(1), stats[statKeyPanelTracesCount])
assert.Equal(t, int64(0), stats[statKeyPanelMetricsCount])
assert.Equal(t, int64(0), stats[statKeyPanelLogsCount])
}
// promql and clickhouse queries carry no signal, so they land in the panel total
// and nowhere else.
func TestNewStatsFromStorableDashboardsIgnoresSignallessQueries(t *testing.T) {

View File

@@ -8,7 +8,6 @@ import (
"testing"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/perses/spec/go/dashboard"
"github.com/stretchr/testify/assert"
@@ -1619,43 +1618,6 @@ func TestStorageRoundTrip(t *testing.T) {
assert.Contains(t, responseStr, `"operator":"above"`, "expected operator:above after storage round-trip")
}
// An AI builder query carries no signal of its own: the plugin kind implies
// gen_ai, which only reads traces, so decode pins the signal and marshal emits it.
func TestAIBuilderQueryStorageRoundTrip(t *testing.T) {
input := []byte(`{
"variables": [],
"panels": {"p1": {"kind": "Panel", "spec": {
"links": [],
"plugin": {"kind": "signoz/TimeSeriesPanel", "spec": {}},
"queries": [{"kind": "time_series", "spec": {"plugin": {"kind": "signoz/AIBuilderQuery", "spec": {
"name": "A", "aggregations": [{"expression": "count()"}]
}}}}]
}}},
"links": [],
"layouts": []
}`)
d, err := unmarshalDashboard(input)
require.NoError(t, err)
plugin := d.Panels["p1"].Spec.Queries[0].Spec.Plugin
assert.Equal(t, QueryKindAIBuilder, plugin.Kind)
aiSpec, ok := plugin.Spec.(*AIBuilderQuerySpec)
require.True(t, ok, "expected *AIBuilderQuerySpec, got %T", plugin.Spec)
assert.Equal(t, "A", aiSpec.Name)
assert.Equal(t, telemetrytypes.SignalTraces, aiSpec.Signal)
stored, err := json.Marshal(plugin)
require.NoError(t, err)
assert.Contains(t, string(stored), `"kind":"signoz/AIBuilderQuery"`)
assert.Contains(t, string(stored), `"signal":"traces"`)
var loaded QueryPlugin
require.NoError(t, json.Unmarshal(stored, &loaded))
assert.Equal(t, plugin, loaded)
}
func TestPostableDashboardV2GenerateNameFlag(t *testing.T) {
const validSpec = `"spec": {"variables": [], "panels": {}, "layouts": [], "links": []}`
@@ -1868,8 +1830,6 @@ func TestPanelTypeQueryTypeCompatibility(t *testing.T) {
{"TimeSeries+PromQL", mkQuery("signoz/TimeSeriesPanel", "signoz/PromQLQuery", `{"name":"A","query":"up"}`), false},
{"Table+ClickHouse", mkQuery("signoz/TablePanel", "signoz/ClickHouseSQL", `{"name":"A","query":"SELECT 1"}`), false},
{"List+Builder", mkQuery("signoz/ListPanel", "signoz/BuilderQuery", `{"name":"A","signal":"logs"}`), false},
{"TimeSeries+AIBuilder", mkQuery("signoz/TimeSeriesPanel", "signoz/AIBuilderQuery", `{"name":"A","aggregations":[{"expression":"count()"}]}`), false},
{"List+AIBuilder", mkQuery("signoz/ListPanel", "signoz/AIBuilderQuery", `{"name":"A"}`), false},
// Top-level: rejected
{"Table+PromQL", mkQuery("signoz/TablePanel", "signoz/PromQLQuery", `{"name":"A","query":"up"}`), true},
{"List+ClickHouse", mkQuery("signoz/ListPanel", "signoz/ClickHouseSQL", `{"name":"A","query":"SELECT 1"}`), true},
@@ -1879,7 +1839,6 @@ func TestPanelTypeQueryTypeCompatibility(t *testing.T) {
// Composite sub-queries
{"Table+Composite(promql)", mkComposite("signoz/TablePanel", "promql", `{"name":"A","query":"up"}`), true},
{"Table+Composite(clickhouse)", mkComposite("signoz/TablePanel", "clickhouse_sql", `{"name":"A","query":"SELECT 1"}`), false},
{"Table+Composite(builder_ai)", mkComposite("signoz/TablePanel", "builder_ai_query", `{"name":"A","aggregations":[{"expression":"count()"}]}`), false},
}
for _, tc := range cases {

View File

@@ -91,7 +91,6 @@ type QueryPlugin struct {
func (QueryPlugin) PrepareJSONSchema(s *jsonschema.Schema) error {
return markDiscriminator(s, "kind", map[string]string{
string(QueryKindBuilder): schemaRef("DashboardtypesQueryPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesBuilderQuerySpec"),
string(QueryKindAIBuilder): schemaRef("DashboardtypesQueryPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesAIBuilderQuerySpec"),
string(QueryKindComposite): schemaRef("DashboardtypesQueryPluginVariantGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5CompositeQuery"),
string(QueryKindFormula): schemaRef("DashboardtypesQueryPluginVariantGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5QueryBuilderFormula"),
string(QueryKindPromQL): schemaRef("DashboardtypesQueryPluginVariantGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5PromQuery"),
@@ -121,7 +120,6 @@ func (p *QueryPlugin) UnmarshalJSON(data []byte) error {
func (QueryPlugin) JSONSchemaOneOf() []any {
return []any{
QueryPluginVariant[BuilderQuerySpec]{Kind: string(QueryKindBuilder)},
QueryPluginVariant[AIBuilderQuerySpec]{Kind: string(QueryKindAIBuilder)},
QueryPluginVariant[CompositeQuerySpec]{Kind: string(QueryKindComposite)},
QueryPluginVariant[FormulaSpec]{Kind: string(QueryKindFormula)},
QueryPluginVariant[PromQLQuerySpec]{Kind: string(QueryKindPromQL)},
@@ -142,11 +140,6 @@ func (plugin QueryPlugin) buildV5CompositeQueryFromPlugin() (qb.CompositeQuery,
return qb.CompositeQuery{}, errors.Newf(errors.TypeInvalidInput, ErrCodeDashboardInvalidWidgetQuery, "builder query is empty")
}
return wrapEnvelope(qb.QueryTypeBuilder, spec.Spec), nil
case *AIBuilderQuerySpec:
if spec == nil {
return qb.CompositeQuery{}, errors.Newf(errors.TypeInvalidInput, ErrCodeDashboardInvalidWidgetQuery, "AI builder query is empty")
}
return wrapEnvelope(qb.QueryTypeBuilderAI, qb.QueryBuilderQuery[qb.TraceAggregation](*spec)), nil
case *qb.PromQuery:
return wrapEnvelope(qb.QueryTypePromQL, *spec), nil
case *qb.ClickHouseQuery:
@@ -241,7 +234,6 @@ var (
}
queryPluginSpecs = map[QueryPluginKind]func() any{
QueryKindBuilder: func() any { return new(BuilderQuerySpec) },
QueryKindAIBuilder: func() any { return new(AIBuilderQuerySpec) },
QueryKindComposite: func() any { return new(CompositeQuerySpec) },
QueryKindFormula: func() any { return new(FormulaSpec) },
QueryKindPromQL: func() any { return new(PromQLQuerySpec) },
@@ -254,13 +246,13 @@ 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},
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},
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},
PanelKindText: {},
}
)

View File

@@ -106,13 +106,6 @@ func redactQuery(spec any) any {
return spec
}
return &BuilderQuerySpec{Spec: redactLeafQuery(s.Spec)}
case *AIBuilderQuerySpec:
if s == nil {
return spec
}
redacted := redactLeafQuery(qb.QueryBuilderQuery[qb.TraceAggregation](*s)).(qb.QueryBuilderQuery[qb.TraceAggregation])
out := AIBuilderQuerySpec(redacted)
return &out
case *qb.PromQuery:
return redactQueryPtr(s)
case *qb.ClickHouseQuery:

View File

@@ -5,7 +5,6 @@ import (
"github.com/SigNoz/signoz/pkg/errors"
qb "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -160,11 +159,6 @@ func TestDashboardV2GetPanelQuery(t *testing.T) {
plugin QueryPlugin
expectedType qb.QueryType
}{
{
description: "AI builder query",
plugin: QueryPlugin{Kind: QueryKindAIBuilder, Spec: &AIBuilderQuerySpec{Name: "A"}},
expectedType: qb.QueryTypeBuilderAI,
},
{
description: "promql",
plugin: QueryPlugin{Kind: QueryKindPromQL, Spec: &qb.PromQuery{Name: "A", Query: "up"}},
@@ -215,42 +209,6 @@ func TestDashboardV2GetPanelQuery(t *testing.T) {
}
})
// The gen_ai statement builder only reads traces, so an AI builder query
// carries no signal of its own and unwraps to a traces builder query.
t.Run("unwraps an AI builder query to a traces builder query", func(t *testing.T) {
dashboard := &DashboardV2{
Spec: DashboardSpec{
Panels: map[string]*Panel{
"panel-1": {
Spec: PanelSpec{
Plugin: PanelPlugin{Kind: PanelKindTimeSeries},
Queries: []Query{
{
Kind: qb.RequestTypeTimeSeries,
Spec: QuerySpec{
Plugin: QueryPlugin{
Kind: QueryKindAIBuilder,
Spec: &AIBuilderQuerySpec{Name: "A", Signal: telemetrytypes.SignalTraces},
},
},
},
},
},
},
},
},
}
req, err := dashboard.GetPanelQuery(1, 2, "panel-1")
require.NoError(t, err)
require.Len(t, req.CompositeQuery.Queries, 1)
spec, ok := req.CompositeQuery.Queries[0].Spec.(qb.QueryBuilderQuery[qb.TraceAggregation])
require.True(t, ok, "expected traces builder query, got %T", req.CompositeQuery.Queries[0].Spec)
assert.Equal(t, "A", spec.Name)
assert.Equal(t, telemetrytypes.SignalTraces, spec.Signal)
})
t.Run("sets FormatTableResultForUI only for table panels", func(t *testing.T) {
dashboard := &DashboardV2{
Spec: DashboardSpec{

View File

@@ -133,19 +133,6 @@ func TestRedactQueryPluginWrappers(t *testing.T) {
assert.Equal(t, "A", builder.Name)
})
t.Run("AI builder plugin pointer is redacted and stays a pointer", func(t *testing.T) {
plugin := &AIBuilderQuerySpec{
Name: "A",
Filter: &qb.Filter{Expression: "body contains 'secret'"},
}
result, ok := redactQuery(plugin).(*AIBuilderQuerySpec)
require.True(t, ok)
assert.Nil(t, result.Filter)
assert.Equal(t, "A", result.Name)
})
t.Run("composite plugin redacts every sub-query envelope", func(t *testing.T) {
composite := &qb.CompositeQuery{Queries: []qb.QueryEnvelope{
{Type: qb.QueryTypeBuilder, Spec: qb.QueryBuilderQuery[qb.MetricAggregation]{Name: "A", Filter: &qb.Filter{Expression: "x = 1"}}},

View File

@@ -93,7 +93,6 @@ type QueryPluginKind string
const (
QueryKindBuilder QueryPluginKind = "signoz/BuilderQuery"
QueryKindAIBuilder QueryPluginKind = "signoz/AIBuilderQuery"
QueryKindComposite QueryPluginKind = "signoz/CompositeQuery"
QueryKindFormula QueryPluginKind = "signoz/Formula"
QueryKindPromQL QueryPluginKind = "signoz/PromQLQuery"
@@ -102,7 +101,7 @@ const (
)
func (QueryPluginKind) Enum() []any {
return []any{QueryKindBuilder, QueryKindAIBuilder, QueryKindComposite, QueryKindFormula, QueryKindPromQL, QueryKindClickHouseSQL, QueryKindTraceOperator}
return []any{QueryKindBuilder, QueryKindComposite, QueryKindFormula, QueryKindPromQL, QueryKindClickHouseSQL, QueryKindTraceOperator}
}
type (
@@ -160,26 +159,6 @@ func (BuilderQuerySpec) JSONSchemaOneOf() []any {
}
}
// AIBuilderQuerySpec is the spec of a signoz/AIBuilderQuery plugin: a gen_ai-scoped
// (AI observability) traces builder query, executed as qb.QueryTypeBuilderAI. The
// signal is implied by the kind and pinned to traces, mirroring the builder_ai_query
// QueryEnvelope decode.
type AIBuilderQuerySpec qb.QueryBuilderQuery[qb.TraceAggregation]
func (b *AIBuilderQuerySpec) UnmarshalJSON(data []byte) error {
var spec qb.QueryBuilderQuery[qb.TraceAggregation]
if err := json.Unmarshal(data, &spec); err != nil {
return errors.WrapInvalidInputf(err, ErrCodeDashboardInvalidInput, "invalid AI builder query spec")
}
spec.Signal = telemetrytypes.SignalTraces
*b = AIBuilderQuerySpec(spec)
return nil
}
func (AIBuilderQuerySpec) PrepareJSONSchema(s *jsonschema.Schema) error {
return (qb.QueryBuilderQuery[qb.TraceAggregation]{}).PrepareJSONSchema(s)
}
// ══════════════════════════════════════════════
// SigNoz panel plugin specs
// ══════════════════════════════════════════════

View File

@@ -0,0 +1,88 @@
import json
from collections.abc import Callable
from datetime import UTC, datetime, timedelta
from http import HTTPStatus
from fixtures import types
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
from fixtures.logs import Logs
from fixtures.querier import (
build_raw_query,
get_rows,
make_query_request,
)
def test_resource_body_conflict(
signoz: types.SigNoz,
create_user_admin: None, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
insert_logs: Callable[[list[Logs]], None],
export_json_types: Callable[[list[Logs]], None],
) -> None:
now = datetime.now(tz=UTC)
start_ms = int((now - timedelta(seconds=10)).timestamp() * 1000)
end_ms = int(now.timestamp() * 1000)
# python's body carries service.name, making the bare key ambiguous across
# resource and body; java's body omits it, so ANDing body in would drop it.
logs_list = [
Logs(
timestamp=now - timedelta(seconds=2),
resources={"service.name": "java"},
body_v2=json.dumps({"msg": "hello"}),
body_promoted="",
),
Logs(
timestamp=now - timedelta(seconds=1),
resources={"service.name": "python"},
body_v2=json.dumps({"service.name": "python"}),
body_promoted="",
),
]
export_json_types(logs_list)
insert_logs(logs_list)
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
cases = [
{
"name": "bare_key_resolves_to_resource",
"filter": "service.name = 'java'",
"expected_service_names": ["java"],
"expect_resource_warning": True,
},
{
"name": "qualified_body_key_targets_body",
"filter": "body.service.name = 'python'",
"expected_service_names": ["python"],
"expect_resource_warning": False,
},
]
for case in cases:
response = make_query_request(
signoz,
token,
start_ms,
end_ms,
request_type="raw",
queries=[
build_raw_query(
name="A",
signal="logs",
filter_expression=case["filter"],
limit=100,
step_interval=60,
)
],
)
assert response.status_code == HTTPStatus.OK, f"{case['name']}: {response.text}"
rows = get_rows(response)
assert [row["data"]["resources_string"].get("service.name") for row in rows] == case["expected_service_names"], f"{case['name']}: {response.json()}"
warning = response.json()["data"].get("warning")
if case["expect_resource_warning"]:
assert warning is not None and "Using `resource` context by default" in warning["warnings"][0]["message"], f"{case['name']}: {warning}"
else:
assert warning is None, f"{case['name']}: {warning}"

View File

@@ -64,8 +64,8 @@ def test_resource_default_warning(
"Key `service.name` is ambiguous, found 2 different combinations of "
"field context / data type: [name=service.name,context=resource,datatype=string "
"name=service.name,context=attribute,datatype=string]. Using `resource` context "
"by default. To query attributes explicitly, use the fully qualified name "
"(e.g., 'attribute.service.name')"
"by default. To query another context explicitly, use the fully qualified name "
"(e.g., 'attribute.service.name' or 'body.service.name')"
)
assert warning["warnings"] == [
{"message": expected_service_name_warning},
@@ -237,8 +237,8 @@ def test_deduped_warnings_for_single_query(
"Key `service.name` is ambiguous, found 2 different combinations of "
"field context / data type: [name=service.name,context=resource,datatype=string "
"name=service.name,context=attribute,datatype=string]. Using `resource` context "
"by default. To query attributes explicitly, use the fully qualified name "
"(e.g., 'attribute.service.name')"
"by default. To query another context explicitly, use the fully qualified name "
"(e.g., 'attribute.service.name' or 'body.service.name')"
)
expected_status_code_warning = "Key `http.status_code` is ambiguous, found 2 different combinations of field context / data type: [name=http.status_code,context=attribute,datatype=number name=http.status_code,context=attribute,datatype=string]."
assert warning["warnings"] == [
@@ -328,8 +328,8 @@ def test_deduped_warnings_for_multiple_queries(
"Key `service.name` is ambiguous, found 2 different combinations of "
"field context / data type: [name=service.name,context=resource,datatype=string "
"name=service.name,context=attribute,datatype=string]. Using `resource` context "
"by default. To query attributes explicitly, use the fully qualified name "
"(e.g., 'attribute.service.name')"
"by default. To query another context explicitly, use the fully qualified name "
"(e.g., 'attribute.service.name' or 'body.service.name')"
)
expected_status_code_warning = "Key `http.status_code` is ambiguous, found 2 different combinations of field context / data type: [name=http.status_code,context=attribute,datatype=number name=http.status_code,context=attribute,datatype=string]."
assert warning["warnings"] == [