Compare commits
80 Commits
SIG-2878
...
demo/trace
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7206bb82fe | ||
|
|
a1ad2b7835 | ||
|
|
a2ab97a347 | ||
|
|
7c1ca7544d | ||
|
|
1b0dcb86b5 | ||
|
|
cb49bc795b | ||
|
|
3f1aeb3077 | ||
|
|
cc2a905e0b | ||
|
|
eba024fc5d | ||
|
|
561ec8fd40 | ||
|
|
aa1dfc6eb1 | ||
|
|
3248012716 | ||
|
|
4ce56ebab4 | ||
|
|
bb80d69819 | ||
|
|
49aaecd02c | ||
|
|
98f4e840cd | ||
|
|
74824e7853 | ||
|
|
b574fee2d4 | ||
|
|
675b66a7b9 | ||
|
|
f55aeb5b5a | ||
|
|
ae3806ce64 | ||
|
|
9c489ebc84 | ||
|
|
f6d432cfce | ||
|
|
6ca6f615b0 | ||
|
|
36e7820edd | ||
|
|
f51cce844b | ||
|
|
b2d3d61b44 | ||
|
|
4e2c7c6309 | ||
|
|
885045d704 | ||
|
|
9dc2e82ce1 | ||
|
|
19e60ee688 | ||
|
|
ea89714cb4 | ||
|
|
4be618bcde | ||
|
|
2bfecce3cb | ||
|
|
eefbcbd1eb | ||
|
|
a3f366ee36 | ||
|
|
cff547c303 | ||
|
|
d6287cba52 | ||
|
|
44b09fbef2 | ||
|
|
081eb64893 | ||
|
|
6338af55dd | ||
|
|
5450b92650 | ||
|
|
a9179321e1 | ||
|
|
90366975d8 | ||
|
|
33f47993d3 | ||
|
|
9170846111 | ||
|
|
54baa9d76d | ||
|
|
0ed6aac74e | ||
|
|
b994fed409 | ||
|
|
a9eb992f67 | ||
|
|
ed95815a6a | ||
|
|
2e2888346f | ||
|
|
525c5ac081 | ||
|
|
66cede4c03 | ||
|
|
33ea94991a | ||
|
|
bae461d1f8 | ||
|
|
9df82cc952 | ||
|
|
d3d927c84d | ||
|
|
36ab1ce8a2 | ||
|
|
7bbf3ffba3 | ||
|
|
6ab5c3cf2e | ||
|
|
c2384e387d | ||
|
|
a00f263bad | ||
|
|
9d648915cc | ||
|
|
e6bd7484fa | ||
|
|
d780c7482e | ||
|
|
ffa8d0267e | ||
|
|
f0505a9c0e | ||
|
|
09e212bd64 | ||
|
|
75f3131e65 | ||
|
|
b1b571ace9 | ||
|
|
876f580f75 | ||
|
|
7999f261ef | ||
|
|
66b8574f74 | ||
|
|
d7b8be11a4 | ||
|
|
aa3935cc31 | ||
|
|
002c755ca5 | ||
|
|
558739b4e7 | ||
|
|
efdfa48ad0 | ||
|
|
693c4451ee |
@@ -5,7 +5,10 @@ import getStartEndRangeTime from 'lib/getStartEndRangeTime';
|
|||||||
import { mapQueryDataToApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataToApi';
|
import { mapQueryDataToApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataToApi';
|
||||||
import { isEmpty } from 'lodash-es';
|
import { isEmpty } from 'lodash-es';
|
||||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
import {
|
||||||
|
IBuilderQuery,
|
||||||
|
IBuilderTraceOperator,
|
||||||
|
} from 'types/api/queryBuilder/queryBuilderData';
|
||||||
import {
|
import {
|
||||||
BaseBuilderQuery,
|
BaseBuilderQuery,
|
||||||
FieldContext,
|
FieldContext,
|
||||||
@@ -276,6 +279,103 @@ export function convertBuilderQueriesToV5(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createTraceOperatorBaseSpec(
|
||||||
|
queryData: IBuilderTraceOperator,
|
||||||
|
requestType: RequestType,
|
||||||
|
panelType?: PANEL_TYPES,
|
||||||
|
): BaseBuilderQuery {
|
||||||
|
const nonEmptySelectColumns = (queryData.selectColumns as (
|
||||||
|
| BaseAutocompleteData
|
||||||
|
| TelemetryFieldKey
|
||||||
|
)[])?.filter((c) => ('key' in c ? c?.key : c?.name));
|
||||||
|
|
||||||
|
return {
|
||||||
|
stepInterval: queryData?.stepInterval || undefined,
|
||||||
|
groupBy:
|
||||||
|
queryData.groupBy?.length > 0
|
||||||
|
? queryData.groupBy.map(
|
||||||
|
(item: any): GroupByKey => ({
|
||||||
|
name: item.key,
|
||||||
|
fieldDataType: item?.dataType,
|
||||||
|
fieldContext: item?.type,
|
||||||
|
description: item?.description,
|
||||||
|
unit: item?.unit,
|
||||||
|
signal: item?.signal,
|
||||||
|
materialized: item?.materialized,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
: undefined,
|
||||||
|
limit:
|
||||||
|
panelType === PANEL_TYPES.TABLE || panelType === PANEL_TYPES.LIST
|
||||||
|
? queryData.limit || queryData.pageSize || undefined
|
||||||
|
: queryData.limit || undefined,
|
||||||
|
offset:
|
||||||
|
requestType === 'raw' || requestType === 'trace'
|
||||||
|
? queryData.offset
|
||||||
|
: undefined,
|
||||||
|
order:
|
||||||
|
queryData.orderBy?.length > 0
|
||||||
|
? queryData.orderBy.map(
|
||||||
|
(order: any): OrderBy => ({
|
||||||
|
key: {
|
||||||
|
name: order.columnName,
|
||||||
|
},
|
||||||
|
direction: order.order,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
: undefined,
|
||||||
|
legend: isEmpty(queryData.legend) ? undefined : queryData.legend,
|
||||||
|
having: isEmpty(queryData.having) ? undefined : (queryData?.having as Having),
|
||||||
|
selectFields: isEmpty(nonEmptySelectColumns)
|
||||||
|
? undefined
|
||||||
|
: nonEmptySelectColumns?.map(
|
||||||
|
(column: any): TelemetryFieldKey => ({
|
||||||
|
name: column.name ?? column.key,
|
||||||
|
fieldDataType:
|
||||||
|
column?.fieldDataType ?? (column?.dataType as FieldDataType),
|
||||||
|
fieldContext: column?.fieldContext ?? (column?.type as FieldContext),
|
||||||
|
signal: column?.signal ?? undefined,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function convertTraceOperatorToV5(
|
||||||
|
traceOperator: Record<string, IBuilderTraceOperator>,
|
||||||
|
requestType: RequestType,
|
||||||
|
panelType?: PANEL_TYPES,
|
||||||
|
): QueryEnvelope[] {
|
||||||
|
return Object.entries(traceOperator).map(
|
||||||
|
([queryName, traceOperatorData]): QueryEnvelope => {
|
||||||
|
const baseSpec = createTraceOperatorBaseSpec(
|
||||||
|
traceOperatorData,
|
||||||
|
requestType,
|
||||||
|
panelType,
|
||||||
|
);
|
||||||
|
let spec: QueryEnvelope['spec'];
|
||||||
|
|
||||||
|
// Skip aggregation for raw request type
|
||||||
|
const aggregations =
|
||||||
|
requestType === 'raw'
|
||||||
|
? undefined
|
||||||
|
: createAggregation(traceOperatorData, panelType);
|
||||||
|
|
||||||
|
spec = {
|
||||||
|
name: queryName,
|
||||||
|
returnSpansFrom: traceOperatorData.returnSpansFrom || '',
|
||||||
|
...baseSpec,
|
||||||
|
expression: traceOperatorData.expression || '',
|
||||||
|
aggregations: aggregations as TraceAggregation[],
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'builder_trace_operator' as QueryType,
|
||||||
|
spec,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts PromQL queries to V5 format
|
* Converts PromQL queries to V5 format
|
||||||
*/
|
*/
|
||||||
@@ -357,14 +457,27 @@ export const prepareQueryRangePayloadV5 = ({
|
|||||||
|
|
||||||
switch (query.queryType) {
|
switch (query.queryType) {
|
||||||
case EQueryType.QUERY_BUILDER: {
|
case EQueryType.QUERY_BUILDER: {
|
||||||
const { queryData: data, queryFormulas } = query.builder;
|
const { queryData: data, queryFormulas, queryTraceOperator } = query.builder;
|
||||||
const currentQueryData = mapQueryDataToApi(data, 'queryName', tableParams);
|
const currentQueryData = mapQueryDataToApi(data, 'queryName', tableParams);
|
||||||
const currentFormulas = mapQueryDataToApi(queryFormulas, 'queryName');
|
const currentFormulas = mapQueryDataToApi(queryFormulas, 'queryName');
|
||||||
|
|
||||||
|
const filteredTraceOperator =
|
||||||
|
queryTraceOperator && queryTraceOperator.length > 0
|
||||||
|
? queryTraceOperator.filter((traceOperator) =>
|
||||||
|
Boolean(traceOperator.expression.trim()),
|
||||||
|
)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const currentTraceOperator = mapQueryDataToApi(
|
||||||
|
filteredTraceOperator,
|
||||||
|
'queryName',
|
||||||
|
);
|
||||||
|
|
||||||
// Combine legend maps
|
// Combine legend maps
|
||||||
legendMap = {
|
legendMap = {
|
||||||
...currentQueryData.newLegendMap,
|
...currentQueryData.newLegendMap,
|
||||||
...currentFormulas.newLegendMap,
|
...currentFormulas.newLegendMap,
|
||||||
|
...currentTraceOperator.newLegendMap,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Convert builder queries
|
// Convert builder queries
|
||||||
@@ -397,8 +510,36 @@ export const prepareQueryRangePayloadV5 = ({
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const traceOperatorQueries = convertTraceOperatorToV5(
|
||||||
|
currentTraceOperator.data,
|
||||||
|
requestType,
|
||||||
|
graphType,
|
||||||
|
);
|
||||||
|
|
||||||
|
// const traceOperatorQueries = Object.entries(currentTraceOperator.data).map(
|
||||||
|
// ([queryName, traceOperatorData]): QueryEnvelope => ({
|
||||||
|
// type: 'builder_trace_operator' as const,
|
||||||
|
// spec: {
|
||||||
|
// name: queryName,
|
||||||
|
// expression: traceOperatorData.expression || '',
|
||||||
|
// legend: isEmpty(traceOperatorData.legend)
|
||||||
|
// ? undefined
|
||||||
|
// : traceOperatorData.legend,
|
||||||
|
// limit: 10,
|
||||||
|
// order: traceOperatorData.orderBy?.map(
|
||||||
|
// // eslint-disable-next-line sonarjs/no-identical-functions
|
||||||
|
// (order: any): OrderBy => ({
|
||||||
|
// key: {
|
||||||
|
// name: order.columnName,
|
||||||
|
// },
|
||||||
|
// direction: order.order,
|
||||||
|
// }),
|
||||||
|
// ),
|
||||||
|
// },
|
||||||
|
// }),
|
||||||
|
// );
|
||||||
// Combine both types
|
// Combine both types
|
||||||
queries = [...builderQueries, ...formulaQueries];
|
queries = [...builderQueries, ...formulaQueries, ...traceOperatorQueries];
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case EQueryType.PROM: {
|
case EQueryType.PROM: {
|
||||||
|
|||||||
@@ -22,6 +22,10 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
|
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
.qb-trace-view-selector-container {
|
||||||
|
padding: 12px 8px 8px 8px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.qb-content-section {
|
.qb-content-section {
|
||||||
@@ -179,7 +183,7 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
|
||||||
margin-left: 32px;
|
margin-left: 26px;
|
||||||
padding-bottom: 16px;
|
padding-bottom: 16px;
|
||||||
padding-left: 8px;
|
padding-left: 8px;
|
||||||
|
|
||||||
@@ -195,8 +199,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.formula-container {
|
.formula-container {
|
||||||
margin-left: 82px;
|
padding: 8px;
|
||||||
padding: 4px 0px;
|
margin-left: 74px;
|
||||||
|
|
||||||
.ant-col {
|
.ant-col {
|
||||||
&::before {
|
&::before {
|
||||||
@@ -331,6 +335,12 @@
|
|||||||
);
|
);
|
||||||
left: 15px;
|
left: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.has-trace-operator {
|
||||||
|
&::before {
|
||||||
|
height: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.formula-name {
|
.formula-name {
|
||||||
@@ -347,7 +357,7 @@
|
|||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
content: '';
|
content: '';
|
||||||
height: 65px;
|
height: 128px;
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
|||||||
@@ -5,11 +5,13 @@ import { Formula } from 'container/QueryBuilder/components/Formula';
|
|||||||
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
|
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
import { memo, useEffect, useMemo, useRef } from 'react';
|
import { memo, useEffect, useMemo, useRef } from 'react';
|
||||||
|
import { IBuilderTraceOperator } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
import { DataSource } from 'types/common/queryBuilder';
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
import { QueryBuilderV2Provider } from './QueryBuilderV2Context';
|
import { QueryBuilderV2Provider } from './QueryBuilderV2Context';
|
||||||
import QueryFooter from './QueryV2/QueryFooter/QueryFooter';
|
import QueryFooter from './QueryV2/QueryFooter/QueryFooter';
|
||||||
import { QueryV2 } from './QueryV2/QueryV2';
|
import { QueryV2 } from './QueryV2/QueryV2';
|
||||||
|
import TraceOperator from './QueryV2/TraceOperator/TraceOperator';
|
||||||
|
|
||||||
export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
||||||
config,
|
config,
|
||||||
@@ -18,6 +20,7 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
|||||||
queryComponents,
|
queryComponents,
|
||||||
isListViewPanel = false,
|
isListViewPanel = false,
|
||||||
showOnlyWhereClause = false,
|
showOnlyWhereClause = false,
|
||||||
|
showTraceOperator = false,
|
||||||
version,
|
version,
|
||||||
}: QueryBuilderProps): JSX.Element {
|
}: QueryBuilderProps): JSX.Element {
|
||||||
const {
|
const {
|
||||||
@@ -25,6 +28,7 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
|||||||
addNewBuilderQuery,
|
addNewBuilderQuery,
|
||||||
addNewFormula,
|
addNewFormula,
|
||||||
handleSetConfig,
|
handleSetConfig,
|
||||||
|
addTraceOperator,
|
||||||
panelType,
|
panelType,
|
||||||
initialDataSource,
|
initialDataSource,
|
||||||
} = useQueryBuilder();
|
} = useQueryBuilder();
|
||||||
@@ -54,6 +58,14 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
|||||||
newPanelType,
|
newPanelType,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const isMultiQueryAllowed = useMemo(
|
||||||
|
() =>
|
||||||
|
!showOnlyWhereClause ||
|
||||||
|
!isListViewPanel ||
|
||||||
|
(currentDataSource === DataSource.TRACES && showTraceOperator),
|
||||||
|
[showOnlyWhereClause, currentDataSource, showTraceOperator, isListViewPanel],
|
||||||
|
);
|
||||||
|
|
||||||
const listViewLogFilterConfigs: QueryBuilderProps['filterConfigs'] = useMemo(() => {
|
const listViewLogFilterConfigs: QueryBuilderProps['filterConfigs'] = useMemo(() => {
|
||||||
const config: QueryBuilderProps['filterConfigs'] = {
|
const config: QueryBuilderProps['filterConfigs'] = {
|
||||||
stepInterval: { isHidden: true, isDisabled: true },
|
stepInterval: { isHidden: true, isDisabled: true },
|
||||||
@@ -97,11 +109,45 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
|||||||
listViewTracesFilterConfigs,
|
listViewTracesFilterConfigs,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const traceOperator = useMemo((): IBuilderTraceOperator | undefined => {
|
||||||
|
if (
|
||||||
|
currentQuery.builder.queryTraceOperator &&
|
||||||
|
currentQuery.builder.queryTraceOperator.length > 0
|
||||||
|
) {
|
||||||
|
return currentQuery.builder.queryTraceOperator[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}, [currentQuery.builder.queryTraceOperator]);
|
||||||
|
|
||||||
|
const shouldShowTraceOperator = useMemo(
|
||||||
|
() =>
|
||||||
|
showTraceOperator &&
|
||||||
|
currentDataSource === DataSource.TRACES &&
|
||||||
|
Boolean(traceOperator),
|
||||||
|
[currentDataSource, showTraceOperator, traceOperator],
|
||||||
|
);
|
||||||
|
|
||||||
|
const shouldShowFooter = useMemo(
|
||||||
|
() =>
|
||||||
|
(!showOnlyWhereClause && !isListViewPanel) ||
|
||||||
|
(currentDataSource === DataSource.TRACES && showTraceOperator),
|
||||||
|
[isListViewPanel, showTraceOperator, showOnlyWhereClause, currentDataSource],
|
||||||
|
);
|
||||||
|
|
||||||
|
const showFormula = useMemo(() => {
|
||||||
|
if (currentDataSource === DataSource.TRACES) {
|
||||||
|
return !isListViewPanel;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}, [isListViewPanel, currentDataSource]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<QueryBuilderV2Provider>
|
<QueryBuilderV2Provider>
|
||||||
<div className="query-builder-v2">
|
<div className="query-builder-v2">
|
||||||
<div className="qb-content-container">
|
<div className="qb-content-container">
|
||||||
{isListViewPanel && (
|
{!isMultiQueryAllowed ? (
|
||||||
<QueryV2
|
<QueryV2
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
key={currentQuery.builder.queryData[0].queryName}
|
key={currentQuery.builder.queryData[0].queryName}
|
||||||
@@ -109,15 +155,15 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
|||||||
query={currentQuery.builder.queryData[0]}
|
query={currentQuery.builder.queryData[0]}
|
||||||
filterConfigs={queryFilterConfigs}
|
filterConfigs={queryFilterConfigs}
|
||||||
queryComponents={queryComponents}
|
queryComponents={queryComponents}
|
||||||
|
isMultiQueryAllowed={isMultiQueryAllowed}
|
||||||
|
showTraceOperator={shouldShowTraceOperator}
|
||||||
version={version}
|
version={version}
|
||||||
isAvailableToDisable={false}
|
isAvailableToDisable={false}
|
||||||
queryVariant={config?.queryVariant || 'dropdown'}
|
queryVariant={config?.queryVariant || 'dropdown'}
|
||||||
showOnlyWhereClause={showOnlyWhereClause}
|
showOnlyWhereClause={showOnlyWhereClause}
|
||||||
isListViewPanel={isListViewPanel}
|
isListViewPanel={isListViewPanel}
|
||||||
/>
|
/>
|
||||||
)}
|
) : (
|
||||||
|
|
||||||
{!isListViewPanel &&
|
|
||||||
currentQuery.builder.queryData.map((query, index) => (
|
currentQuery.builder.queryData.map((query, index) => (
|
||||||
<QueryV2
|
<QueryV2
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
@@ -127,13 +173,16 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
|||||||
filterConfigs={queryFilterConfigs}
|
filterConfigs={queryFilterConfigs}
|
||||||
queryComponents={queryComponents}
|
queryComponents={queryComponents}
|
||||||
version={version}
|
version={version}
|
||||||
|
isMultiQueryAllowed={isMultiQueryAllowed}
|
||||||
isAvailableToDisable={false}
|
isAvailableToDisable={false}
|
||||||
|
showTraceOperator={shouldShowTraceOperator}
|
||||||
queryVariant={config?.queryVariant || 'dropdown'}
|
queryVariant={config?.queryVariant || 'dropdown'}
|
||||||
showOnlyWhereClause={showOnlyWhereClause}
|
showOnlyWhereClause={showOnlyWhereClause}
|
||||||
isListViewPanel={isListViewPanel}
|
isListViewPanel={isListViewPanel}
|
||||||
signalSource={config?.signalSource || ''}
|
signalSource={config?.signalSource || ''}
|
||||||
/>
|
/>
|
||||||
))}
|
))
|
||||||
|
)}
|
||||||
|
|
||||||
{!showOnlyWhereClause && currentQuery.builder.queryFormulas.length > 0 && (
|
{!showOnlyWhereClause && currentQuery.builder.queryFormulas.length > 0 && (
|
||||||
<div className="qb-formulas-container">
|
<div className="qb-formulas-container">
|
||||||
@@ -158,15 +207,25 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!showOnlyWhereClause && !isListViewPanel && (
|
{shouldShowFooter && (
|
||||||
<QueryFooter
|
<QueryFooter
|
||||||
|
showAddFormula={showFormula}
|
||||||
addNewBuilderQuery={addNewBuilderQuery}
|
addNewBuilderQuery={addNewBuilderQuery}
|
||||||
addNewFormula={addNewFormula}
|
addNewFormula={addNewFormula}
|
||||||
|
addTraceOperator={addTraceOperator}
|
||||||
|
showAddTraceOperator={showTraceOperator && !traceOperator}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{shouldShowTraceOperator && (
|
||||||
|
<TraceOperator
|
||||||
|
isListViewPanel={isListViewPanel}
|
||||||
|
traceOperator={traceOperator as IBuilderTraceOperator}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!showOnlyWhereClause && !isListViewPanel && (
|
{isMultiQueryAllowed && (
|
||||||
<div className="query-names-section">
|
<div className="query-names-section">
|
||||||
{currentQuery.builder.queryData.map((query) => (
|
{currentQuery.builder.queryData.map((query) => (
|
||||||
<div key={query.queryName} className="query-name">
|
<div key={query.queryName} className="query-name">
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
|
.query-add-ons {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.add-ons-list {
|
.add-ons-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
.add-ons-tabs {
|
.add-ons-tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -144,6 +144,8 @@ function QueryAddOns({
|
|||||||
showReduceTo,
|
showReduceTo,
|
||||||
panelType,
|
panelType,
|
||||||
index,
|
index,
|
||||||
|
isForTraceOperator = false,
|
||||||
|
children,
|
||||||
}: {
|
}: {
|
||||||
query: IBuilderQuery;
|
query: IBuilderQuery;
|
||||||
version: string;
|
version: string;
|
||||||
@@ -151,6 +153,8 @@ function QueryAddOns({
|
|||||||
showReduceTo: boolean;
|
showReduceTo: boolean;
|
||||||
panelType: PANEL_TYPES | null;
|
panelType: PANEL_TYPES | null;
|
||||||
index: number;
|
index: number;
|
||||||
|
isForTraceOperator?: boolean;
|
||||||
|
children?: React.ReactNode;
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
const [addOns, setAddOns] = useState<AddOn[]>(ADD_ONS);
|
const [addOns, setAddOns] = useState<AddOn[]>(ADD_ONS);
|
||||||
|
|
||||||
@@ -160,6 +164,7 @@ function QueryAddOns({
|
|||||||
index,
|
index,
|
||||||
query,
|
query,
|
||||||
entityVersion: '',
|
entityVersion: '',
|
||||||
|
isForTraceOperator,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { handleSetQueryData } = useQueryBuilder();
|
const { handleSetQueryData } = useQueryBuilder();
|
||||||
@@ -486,6 +491,7 @@ function QueryAddOns({
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
))}
|
))}
|
||||||
</Radio.Group>
|
</Radio.Group>
|
||||||
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,7 +4,10 @@ import { Tooltip } from 'antd';
|
|||||||
import InputWithLabel from 'components/InputWithLabel/InputWithLabel';
|
import InputWithLabel from 'components/InputWithLabel/InputWithLabel';
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
import {
|
||||||
|
IBuilderQuery,
|
||||||
|
IBuilderTraceOperator,
|
||||||
|
} from 'types/api/queryBuilder/queryBuilderData';
|
||||||
import { DataSource } from 'types/common/queryBuilder';
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
import QueryAggregationSelect from './QueryAggregationSelect';
|
import QueryAggregationSelect from './QueryAggregationSelect';
|
||||||
@@ -20,7 +23,7 @@ function QueryAggregationOptions({
|
|||||||
panelType?: string;
|
panelType?: string;
|
||||||
onAggregationIntervalChange: (value: number) => void;
|
onAggregationIntervalChange: (value: number) => void;
|
||||||
onChange?: (value: string) => void;
|
onChange?: (value: string) => void;
|
||||||
queryData: IBuilderQuery;
|
queryData: IBuilderQuery | IBuilderTraceOperator;
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
const showAggregationInterval = useMemo(() => {
|
const showAggregationInterval = useMemo(() => {
|
||||||
// eslint-disable-next-line sonarjs/prefer-single-boolean-return
|
// eslint-disable-next-line sonarjs/prefer-single-boolean-return
|
||||||
|
|||||||
@@ -4,9 +4,15 @@ import { Plus, Sigma } from 'lucide-react';
|
|||||||
export default function QueryFooter({
|
export default function QueryFooter({
|
||||||
addNewBuilderQuery,
|
addNewBuilderQuery,
|
||||||
addNewFormula,
|
addNewFormula,
|
||||||
|
addTraceOperator,
|
||||||
|
showAddFormula = true,
|
||||||
|
showAddTraceOperator = false,
|
||||||
}: {
|
}: {
|
||||||
addNewBuilderQuery: () => void;
|
addNewBuilderQuery: () => void;
|
||||||
addNewFormula: () => void;
|
addNewFormula: () => void;
|
||||||
|
addTraceOperator?: () => void;
|
||||||
|
showAddTraceOperator: boolean;
|
||||||
|
showAddFormula?: boolean;
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<div className="qb-footer">
|
<div className="qb-footer">
|
||||||
@@ -22,32 +28,62 @@ export default function QueryFooter({
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="qb-add-formula">
|
{showAddFormula && (
|
||||||
<Tooltip
|
<div className="qb-add-formula">
|
||||||
title={
|
<Tooltip
|
||||||
<div style={{ textAlign: 'center' }}>
|
title={
|
||||||
Add New Formula
|
<div style={{ textAlign: 'center' }}>
|
||||||
<Typography.Link
|
Add New Formula
|
||||||
href="https://signoz.io/docs/userguide/query-builder-v5/#multi-query-analysis-advanced-comparisons"
|
<Typography.Link
|
||||||
target="_blank"
|
href="https://signoz.io/docs/userguide/query-builder-v5/#multi-query-analysis-advanced-comparisons"
|
||||||
style={{ textDecoration: 'underline' }}
|
target="_blank"
|
||||||
>
|
style={{ textDecoration: 'underline' }}
|
||||||
{' '}
|
>
|
||||||
<br />
|
{' '}
|
||||||
Learn more
|
<br />
|
||||||
</Typography.Link>
|
Learn more
|
||||||
</div>
|
</Typography.Link>
|
||||||
}
|
</div>
|
||||||
>
|
}
|
||||||
<Button
|
|
||||||
className="add-formula-button periscope-btn secondary"
|
|
||||||
icon={<Sigma size={16} />}
|
|
||||||
onClick={addNewFormula}
|
|
||||||
>
|
>
|
||||||
Add Formula
|
<Button
|
||||||
</Button>
|
className="add-formula-button periscope-btn secondary"
|
||||||
</Tooltip>
|
icon={<Sigma size={16} />}
|
||||||
</div>
|
onClick={addNewFormula}
|
||||||
|
>
|
||||||
|
Add Formula
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{showAddTraceOperator && (
|
||||||
|
<div className="qb-add-formula">
|
||||||
|
<Tooltip
|
||||||
|
title={
|
||||||
|
<div style={{ textAlign: 'center' }}>
|
||||||
|
Add Trace Matching
|
||||||
|
<Typography.Link
|
||||||
|
href="https://signoz.io/docs/userguide/query-builder-v5/#multi-query-analysis-advanced-comparisons"
|
||||||
|
target="_blank"
|
||||||
|
style={{ textDecoration: 'underline' }}
|
||||||
|
>
|
||||||
|
{' '}
|
||||||
|
<br />
|
||||||
|
Learn more
|
||||||
|
</Typography.Link>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
className="add-formula-button periscope-btn secondary"
|
||||||
|
icon={<Sigma size={16} />}
|
||||||
|
onClick={() => addTraceOperator?.()}
|
||||||
|
>
|
||||||
|
Add Trace Matching
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
'Helvetica Neue', sans-serif;
|
'Helvetica Neue', sans-serif;
|
||||||
|
|
||||||
.query-where-clause-editor-container {
|
.query-where-clause-editor-container {
|
||||||
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
||||||
|
|||||||
@@ -26,9 +26,11 @@ export const QueryV2 = memo(function QueryV2({
|
|||||||
query,
|
query,
|
||||||
filterConfigs,
|
filterConfigs,
|
||||||
isListViewPanel = false,
|
isListViewPanel = false,
|
||||||
|
showTraceOperator = false,
|
||||||
version,
|
version,
|
||||||
showOnlyWhereClause = false,
|
showOnlyWhereClause = false,
|
||||||
signalSource = '',
|
signalSource = '',
|
||||||
|
isMultiQueryAllowed = false,
|
||||||
}: QueryProps & { ref: React.RefObject<HTMLDivElement> }): JSX.Element {
|
}: QueryProps & { ref: React.RefObject<HTMLDivElement> }): JSX.Element {
|
||||||
const { cloneQuery, panelType } = useQueryBuilder();
|
const { cloneQuery, panelType } = useQueryBuilder();
|
||||||
|
|
||||||
@@ -108,11 +110,15 @@ export const QueryV2 = memo(function QueryV2({
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
>
|
>
|
||||||
<div className="qb-content-section">
|
<div className="qb-content-section">
|
||||||
{!showOnlyWhereClause && (
|
{isMultiQueryAllowed && (
|
||||||
<div className="qb-header-container">
|
<div className="qb-header-container">
|
||||||
<div className="query-actions-container">
|
<div className="query-actions-container">
|
||||||
<div className="query-actions-left-container">
|
<div className="query-actions-left-container">
|
||||||
<QBEntityOptions
|
<QBEntityOptions
|
||||||
|
hasTraceOperator={
|
||||||
|
showTraceOperator ||
|
||||||
|
(isListViewPanel && dataSource === DataSource.TRACES)
|
||||||
|
}
|
||||||
isMetricsDataSource={dataSource === DataSource.METRICS}
|
isMetricsDataSource={dataSource === DataSource.METRICS}
|
||||||
showFunctions={
|
showFunctions={
|
||||||
(version && version === ENTITY_VERSION_V4) ||
|
(version && version === ENTITY_VERSION_V4) ||
|
||||||
@@ -139,7 +145,30 @@ export const QueryV2 = memo(function QueryV2({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!isListViewPanel && (
|
{!isCollapsed &&
|
||||||
|
(showTraceOperator ||
|
||||||
|
(isListViewPanel && dataSource === DataSource.TRACES)) && (
|
||||||
|
<div className="qb-search-filter-container" style={{ flex: 1 }}>
|
||||||
|
<div className="query-search-container">
|
||||||
|
<QuerySearch
|
||||||
|
key={`query-search-${query.queryName}-${query.dataSource}`}
|
||||||
|
onChange={handleSearchChange}
|
||||||
|
queryData={query}
|
||||||
|
dataSource={dataSource}
|
||||||
|
signalSource={signalSource}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showSpanScopeSelector && (
|
||||||
|
<div className="traces-search-filter-container">
|
||||||
|
<div className="traces-search-filter-in">in</div>
|
||||||
|
<SpanScopeSelector query={query} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isMultiQueryAllowed && (
|
||||||
<Dropdown
|
<Dropdown
|
||||||
className="query-actions-dropdown"
|
className="query-actions-dropdown"
|
||||||
menu={{
|
menu={{
|
||||||
@@ -181,28 +210,32 @@ export const QueryV2 = memo(function QueryV2({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="qb-search-filter-container">
|
{!showTraceOperator &&
|
||||||
<div className="query-search-container">
|
!(isListViewPanel && dataSource === DataSource.TRACES) && (
|
||||||
<QuerySearch
|
<div className="qb-search-filter-container">
|
||||||
key={`query-search-${query.queryName}-${query.dataSource}`}
|
<div className="query-search-container">
|
||||||
onChange={handleSearchChange}
|
<QuerySearch
|
||||||
queryData={query}
|
key={`query-search-${query.queryName}-${query.dataSource}`}
|
||||||
dataSource={dataSource}
|
onChange={handleSearchChange}
|
||||||
signalSource={signalSource}
|
queryData={query}
|
||||||
/>
|
dataSource={dataSource}
|
||||||
</div>
|
signalSource={signalSource}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{showSpanScopeSelector && (
|
{showSpanScopeSelector && (
|
||||||
<div className="traces-search-filter-container">
|
<div className="traces-search-filter-container">
|
||||||
<div className="traces-search-filter-in">in</div>
|
<div className="traces-search-filter-in">in</div>
|
||||||
<SpanScopeSelector query={query} />
|
<SpanScopeSelector query={query} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!showOnlyWhereClause &&
|
{!showOnlyWhereClause &&
|
||||||
!isListViewPanel &&
|
!isListViewPanel &&
|
||||||
|
!showTraceOperator &&
|
||||||
dataSource !== DataSource.METRICS && (
|
dataSource !== DataSource.METRICS && (
|
||||||
<QueryAggregation
|
<QueryAggregation
|
||||||
dataSource={dataSource}
|
dataSource={dataSource}
|
||||||
@@ -225,7 +258,7 @@ export const QueryV2 = memo(function QueryV2({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!showOnlyWhereClause && (
|
{!showOnlyWhereClause && !isListViewPanel && !showTraceOperator && (
|
||||||
<QueryAddOns
|
<QueryAddOns
|
||||||
index={index}
|
index={index}
|
||||||
query={query}
|
query={query}
|
||||||
|
|||||||
@@ -0,0 +1,180 @@
|
|||||||
|
.qb-trace-operator {
|
||||||
|
padding: 8px;
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
&.non-list-view {
|
||||||
|
padding-left: 40px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
left: 12px;
|
||||||
|
height: calc(100% - 48px);
|
||||||
|
width: 1px;
|
||||||
|
background: repeating-linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
#1d212d,
|
||||||
|
#1d212d 4px,
|
||||||
|
transparent 4px,
|
||||||
|
transparent 8px
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-span-source-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
height: 24px;
|
||||||
|
|
||||||
|
&-query {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--bg-vanilla-100);
|
||||||
|
}
|
||||||
|
&-query-name {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
display: grid;
|
||||||
|
place-content: center;
|
||||||
|
padding: 2px;
|
||||||
|
|
||||||
|
border-radius: 2px;
|
||||||
|
border: 1px solid rgba(242, 71, 105, 0.2);
|
||||||
|
background: rgba(242, 71, 105, 0.1);
|
||||||
|
color: var(--Sakura-400, #f56c87);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-arrow {
|
||||||
|
position: relative;
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
left: -26px;
|
||||||
|
height: 1px;
|
||||||
|
width: 20px;
|
||||||
|
background: repeating-linear-gradient(
|
||||||
|
to right,
|
||||||
|
#1d212d,
|
||||||
|
#1d212d 4px,
|
||||||
|
transparent 4px,
|
||||||
|
transparent 8px
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: -10px;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
height: 4px;
|
||||||
|
width: 4px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: var(--bg-slate-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-container {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-aggregation-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-add-ons-container {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-add-ons-input {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
border-radius: 2px;
|
||||||
|
border: 1px solid var(--bg-slate-400);
|
||||||
|
background: var(--bg-ink-300);
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: -16px;
|
||||||
|
top: 50%;
|
||||||
|
height: 1px;
|
||||||
|
width: 16px;
|
||||||
|
background-color: var(--bg-slate-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
padding: 0px 8px;
|
||||||
|
border-right: 1px solid var(--bg-slate-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.qb-trace-operator {
|
||||||
|
&-arrow {
|
||||||
|
&::before {
|
||||||
|
background: repeating-linear-gradient(
|
||||||
|
to right,
|
||||||
|
var(--bg-vanilla-300),
|
||||||
|
var(--bg-vanilla-300) 4px,
|
||||||
|
transparent 4px,
|
||||||
|
transparent 8px
|
||||||
|
);
|
||||||
|
}
|
||||||
|
&::after {
|
||||||
|
background-color: var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.non-list-view {
|
||||||
|
&::before {
|
||||||
|
background: repeating-linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
var(--bg-vanilla-300),
|
||||||
|
var(--bg-vanilla-300) 4px,
|
||||||
|
transparent 4px,
|
||||||
|
transparent 8px
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-add-ons-input {
|
||||||
|
border: 1px solid var(--bg-vanilla-300) !important;
|
||||||
|
background: var(--bg-vanilla-100) !important;
|
||||||
|
|
||||||
|
.label {
|
||||||
|
color: var(--bg-ink-500) !important;
|
||||||
|
border-right: 1px solid var(--bg-vanilla-300) !important;
|
||||||
|
background: var(--bg-vanilla-100) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
/* eslint-disable react/require-default-props */
|
||||||
|
/* eslint-disable sonarjs/no-duplicate-string */
|
||||||
|
|
||||||
|
import './TraceOperator.styles.scss';
|
||||||
|
|
||||||
|
import { Button, Select, Tooltip, Typography } from 'antd';
|
||||||
|
import cx from 'classnames';
|
||||||
|
import InputWithLabel from 'components/InputWithLabel/InputWithLabel';
|
||||||
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
|
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||||
|
import { Trash2 } from 'lucide-react';
|
||||||
|
import { useCallback, useMemo } from 'react';
|
||||||
|
import {
|
||||||
|
IBuilderQuery,
|
||||||
|
IBuilderTraceOperator,
|
||||||
|
} from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
|
import QueryAddOns from '../QueryAddOns/QueryAddOns';
|
||||||
|
import QueryAggregation from '../QueryAggregation/QueryAggregation';
|
||||||
|
|
||||||
|
export default function TraceOperator({
|
||||||
|
traceOperator,
|
||||||
|
isListViewPanel = false,
|
||||||
|
}: {
|
||||||
|
traceOperator: IBuilderTraceOperator;
|
||||||
|
isListViewPanel?: boolean;
|
||||||
|
}): JSX.Element {
|
||||||
|
const { panelType, currentQuery, removeTraceOperator } = useQueryBuilder();
|
||||||
|
const { handleChangeQueryData } = useQueryOperations({
|
||||||
|
index: 0,
|
||||||
|
query: traceOperator,
|
||||||
|
entityVersion: '',
|
||||||
|
isForTraceOperator: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleTraceOperatorChange = useCallback(
|
||||||
|
(traceOperatorExpression: string) => {
|
||||||
|
handleChangeQueryData('expression', traceOperatorExpression);
|
||||||
|
},
|
||||||
|
[handleChangeQueryData],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleChangeAggregateEvery = useCallback(
|
||||||
|
(value: IBuilderQuery['stepInterval']) => {
|
||||||
|
handleChangeQueryData('stepInterval', value);
|
||||||
|
},
|
||||||
|
[handleChangeQueryData],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleChangeAggregation = useCallback(
|
||||||
|
(value: string) => {
|
||||||
|
handleChangeQueryData('aggregations', [
|
||||||
|
{
|
||||||
|
expression: value,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
[handleChangeQueryData],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleChangeSpanSource = useCallback(
|
||||||
|
(value: string) => {
|
||||||
|
handleChangeQueryData('returnSpansFrom', value);
|
||||||
|
},
|
||||||
|
[handleChangeQueryData],
|
||||||
|
);
|
||||||
|
|
||||||
|
const defaultSpanSource = useMemo(
|
||||||
|
() =>
|
||||||
|
traceOperator.returnSpansFrom ||
|
||||||
|
currentQuery.builder.queryData[0].queryName ||
|
||||||
|
'',
|
||||||
|
[currentQuery.builder.queryData, traceOperator?.returnSpansFrom],
|
||||||
|
);
|
||||||
|
|
||||||
|
const spanSourceOptions = useMemo(
|
||||||
|
() =>
|
||||||
|
currentQuery.builder.queryData.map((query) => ({
|
||||||
|
value: query.queryName,
|
||||||
|
label: (
|
||||||
|
<div className="qb-trace-operator-span-source-label">
|
||||||
|
<span className="qb-trace-operator-span-source-label-query">Query</span>
|
||||||
|
<p className="qb-trace-operator-span-source-label-query-name">
|
||||||
|
{query.queryName}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
})),
|
||||||
|
[currentQuery.builder.queryData],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cx('qb-trace-operator', !isListViewPanel && 'non-list-view')}>
|
||||||
|
<div className="qb-trace-operator-container">
|
||||||
|
<InputWithLabel
|
||||||
|
className={cx(
|
||||||
|
'qb-trace-operator-input',
|
||||||
|
!isListViewPanel && 'qb-trace-operator-arrow',
|
||||||
|
)}
|
||||||
|
initialValue={traceOperator?.expression || ''}
|
||||||
|
label="TRACES MATCHING"
|
||||||
|
placeholder="Add condition..."
|
||||||
|
type="text"
|
||||||
|
onChange={handleTraceOperatorChange}
|
||||||
|
/>
|
||||||
|
{!isListViewPanel && (
|
||||||
|
<div className="qb-trace-operator-aggregation-container">
|
||||||
|
<div className={cx(!isListViewPanel && 'qb-trace-operator-arrow')}>
|
||||||
|
<QueryAggregation
|
||||||
|
dataSource={DataSource.TRACES}
|
||||||
|
key={`query-search-${traceOperator.queryName}`}
|
||||||
|
panelType={panelType || undefined}
|
||||||
|
onAggregationIntervalChange={handleChangeAggregateEvery}
|
||||||
|
onChange={handleChangeAggregation}
|
||||||
|
queryData={traceOperator}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={cx(
|
||||||
|
'qb-trace-operator-add-ons-container',
|
||||||
|
!isListViewPanel && 'qb-trace-operator-arrow',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<QueryAddOns
|
||||||
|
index={0}
|
||||||
|
query={traceOperator}
|
||||||
|
version="v3"
|
||||||
|
isForTraceOperator
|
||||||
|
isListViewPanel={false}
|
||||||
|
showReduceTo={false}
|
||||||
|
panelType={panelType}
|
||||||
|
>
|
||||||
|
<div className="qb-trace-operator-add-ons-input">
|
||||||
|
<Typography.Text className="label">Using spans from</Typography.Text>
|
||||||
|
<Select
|
||||||
|
bordered={false}
|
||||||
|
defaultValue={defaultSpanSource}
|
||||||
|
style={{ minWidth: 120 }}
|
||||||
|
onChange={handleChangeSpanSource}
|
||||||
|
options={spanSourceOptions}
|
||||||
|
listItemHeight={24}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</QueryAddOns>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Tooltip title="Remove Trace Operator" placement="topLeft">
|
||||||
|
<Button className="periscope-btn ghost" onClick={removeTraceOperator}>
|
||||||
|
<Trash2 size={14} />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -17,6 +17,19 @@
|
|||||||
font-weight: var(--font-weight-normal);
|
font-weight: var(--font-weight-normal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.view-title-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.icon-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.tab {
|
.tab {
|
||||||
border: 1px solid var(--bg-slate-400);
|
border: 1px solid var(--bg-slate-400);
|
||||||
&:hover {
|
&:hover {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { RadioChangeEvent } from 'antd/es/radio';
|
|||||||
interface Option {
|
interface Option {
|
||||||
value: string;
|
value: string;
|
||||||
label: string;
|
label: string;
|
||||||
|
icon?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SignozRadioGroupProps {
|
interface SignozRadioGroupProps {
|
||||||
@@ -37,7 +38,10 @@ function SignozRadioGroup({
|
|||||||
value={option.value}
|
value={option.value}
|
||||||
className={value === option.value ? 'selected_view tab' : 'tab'}
|
className={value === option.value ? 'selected_view tab' : 'tab'}
|
||||||
>
|
>
|
||||||
{option.label}
|
<div className="view-title-container">
|
||||||
|
{option.icon && <div className="icon-container">{option.icon}</div>}
|
||||||
|
{option.label}
|
||||||
|
</div>
|
||||||
</Radio.Button>
|
</Radio.Button>
|
||||||
))}
|
))}
|
||||||
</Radio.Group>
|
</Radio.Group>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
HavingForm,
|
HavingForm,
|
||||||
IBuilderFormula,
|
IBuilderFormula,
|
||||||
IBuilderQuery,
|
IBuilderQuery,
|
||||||
|
IBuilderTraceOperator,
|
||||||
IClickHouseQuery,
|
IClickHouseQuery,
|
||||||
IPromQLQuery,
|
IPromQLQuery,
|
||||||
Query,
|
Query,
|
||||||
@@ -50,6 +51,8 @@ import {
|
|||||||
export const MAX_FORMULAS = 20;
|
export const MAX_FORMULAS = 20;
|
||||||
export const MAX_QUERIES = 26;
|
export const MAX_QUERIES = 26;
|
||||||
|
|
||||||
|
export const TRACE_OPERATOR_QUERY_NAME = 'T1';
|
||||||
|
|
||||||
export const idDivider = '--';
|
export const idDivider = '--';
|
||||||
export const selectValueDivider = '__';
|
export const selectValueDivider = '__';
|
||||||
|
|
||||||
@@ -265,6 +268,11 @@ export const initialFormulaBuilderFormValues: IBuilderFormula = {
|
|||||||
legend: '',
|
legend: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const initialQueryBuilderFormTraceOperatorValues: IBuilderTraceOperator = {
|
||||||
|
...initialQueryBuilderFormTracesValues,
|
||||||
|
queryName: TRACE_OPERATOR_QUERY_NAME,
|
||||||
|
};
|
||||||
|
|
||||||
export const initialQueryPromQLData: IPromQLQuery = {
|
export const initialQueryPromQLData: IPromQLQuery = {
|
||||||
name: createNewBuilderItemName({ existNames: [], sourceNames: alphabet }),
|
name: createNewBuilderItemName({ existNames: [], sourceNames: alphabet }),
|
||||||
query: '',
|
query: '',
|
||||||
@@ -282,6 +290,7 @@ export const initialClickHouseData: IClickHouseQuery = {
|
|||||||
export const initialQueryBuilderData: QueryBuilderData = {
|
export const initialQueryBuilderData: QueryBuilderData = {
|
||||||
queryData: [initialQueryBuilderFormValues],
|
queryData: [initialQueryBuilderFormValues],
|
||||||
queryFormulas: [],
|
queryFormulas: [],
|
||||||
|
queryTraceOperator: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const initialSingleQueryMap: Record<
|
export const initialSingleQueryMap: Record<
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ function QuerySection({
|
|||||||
queryVariant: 'static',
|
queryVariant: 'static',
|
||||||
initialDataSource: ALERTS_DATA_SOURCE_MAP[alertType],
|
initialDataSource: ALERTS_DATA_SOURCE_MAP[alertType],
|
||||||
}}
|
}}
|
||||||
|
showTraceOperator={alertType === AlertTypes.TRACES_BASED_ALERT}
|
||||||
showFunctions={
|
showFunctions={
|
||||||
(alertType === AlertTypes.METRICS_BASED_ALERT &&
|
(alertType === AlertTypes.METRICS_BASED_ALERT &&
|
||||||
alertDef.version === ENTITY_VERSION_V4) ||
|
alertDef.version === ENTITY_VERSION_V4) ||
|
||||||
|
|||||||
@@ -150,6 +150,7 @@ function FormAlertRules({
|
|||||||
|
|
||||||
const queryOptions = useMemo(() => {
|
const queryOptions = useMemo(() => {
|
||||||
const queryConfig: Record<EQueryType, () => SelectProps['options']> = {
|
const queryConfig: Record<EQueryType, () => SelectProps['options']> = {
|
||||||
|
// TODO: Filter out queries who are used in trace operator
|
||||||
[EQueryType.QUERY_BUILDER]: () => [
|
[EQueryType.QUERY_BUILDER]: () => [
|
||||||
...(getSelectedQueryOptions(currentQuery.builder.queryData) || []),
|
...(getSelectedQueryOptions(currentQuery.builder.queryData) || []),
|
||||||
...(getSelectedQueryOptions(currentQuery.builder.queryFormulas) || []),
|
...(getSelectedQueryOptions(currentQuery.builder.queryFormulas) || []),
|
||||||
|
|||||||
@@ -30,5 +30,14 @@ export type QueryBuilderProps = {
|
|||||||
isListViewPanel?: boolean;
|
isListViewPanel?: boolean;
|
||||||
showFunctions?: boolean;
|
showFunctions?: boolean;
|
||||||
showOnlyWhereClause?: boolean;
|
showOnlyWhereClause?: boolean;
|
||||||
|
showOnlyTraceOperator?: boolean;
|
||||||
|
showTraceViewSelector?: boolean;
|
||||||
|
showTraceOperator?: boolean;
|
||||||
version: string;
|
version: string;
|
||||||
|
onChangeTraceView?: (view: TraceView) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export enum TraceView {
|
||||||
|
SPANS = 'spans',
|
||||||
|
TRACES = 'traces',
|
||||||
|
}
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ interface QBEntityOptionsProps {
|
|||||||
showCloneOption?: boolean;
|
showCloneOption?: boolean;
|
||||||
isListViewPanel?: boolean;
|
isListViewPanel?: boolean;
|
||||||
index?: number;
|
index?: number;
|
||||||
|
hasTraceOperator?: boolean;
|
||||||
queryVariant?: 'dropdown' | 'static';
|
queryVariant?: 'dropdown' | 'static';
|
||||||
onChangeDataSource?: (value: DataSource) => void;
|
onChangeDataSource?: (value: DataSource) => void;
|
||||||
}
|
}
|
||||||
@@ -61,6 +62,7 @@ export default function QBEntityOptions({
|
|||||||
onCloneQuery,
|
onCloneQuery,
|
||||||
index,
|
index,
|
||||||
queryVariant,
|
queryVariant,
|
||||||
|
hasTraceOperator = false,
|
||||||
onChangeDataSource,
|
onChangeDataSource,
|
||||||
}: QBEntityOptionsProps): JSX.Element {
|
}: QBEntityOptionsProps): JSX.Element {
|
||||||
const handleCloneEntity = (): void => {
|
const handleCloneEntity = (): void => {
|
||||||
@@ -97,7 +99,7 @@ export default function QBEntityOptions({
|
|||||||
value="query-builder"
|
value="query-builder"
|
||||||
className="periscope-btn visibility-toggle"
|
className="periscope-btn visibility-toggle"
|
||||||
onClick={onToggleVisibility}
|
onClick={onToggleVisibility}
|
||||||
disabled={isListViewPanel}
|
disabled={isListViewPanel && query?.dataSource !== DataSource.TRACES}
|
||||||
>
|
>
|
||||||
{entityData.disabled ? <EyeOff size={16} /> : <Eye size={16} />}
|
{entityData.disabled ? <EyeOff size={16} /> : <Eye size={16} />}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -115,6 +117,7 @@ export default function QBEntityOptions({
|
|||||||
className={cx(
|
className={cx(
|
||||||
'periscope-btn',
|
'periscope-btn',
|
||||||
entityType === 'query' ? 'query-name' : 'formula-name',
|
entityType === 'query' ? 'query-name' : 'formula-name',
|
||||||
|
hasTraceOperator && 'has-trace-operator',
|
||||||
isLogsExplorerPage && lastUsedQuery === index ? 'sync-btn' : '',
|
isLogsExplorerPage && lastUsedQuery === index ? 'sync-btn' : '',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -11,5 +11,7 @@ export type QueryProps = {
|
|||||||
version: string;
|
version: string;
|
||||||
showSpanScopeSelector?: boolean;
|
showSpanScopeSelector?: boolean;
|
||||||
showOnlyWhereClause?: boolean;
|
showOnlyWhereClause?: boolean;
|
||||||
|
showTraceOperator?: boolean;
|
||||||
signalSource?: string;
|
signalSource?: string;
|
||||||
|
isMultiQueryAllowed?: boolean;
|
||||||
} & Pick<QueryBuilderProps, 'filterConfigs' | 'queryComponents'>;
|
} & Pick<QueryBuilderProps, 'filterConfigs' | 'queryComponents'>;
|
||||||
|
|||||||
@@ -37,11 +37,15 @@ function QuerySection(): JSX.Element {
|
|||||||
};
|
};
|
||||||
}, [panelTypes, renderOrderBy]);
|
}, [panelTypes, renderOrderBy]);
|
||||||
|
|
||||||
|
const isListViewPanel = useMemo(
|
||||||
|
() => panelTypes === PANEL_TYPES.LIST || panelTypes === PANEL_TYPES.TRACE,
|
||||||
|
[panelTypes],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<QueryBuilderV2
|
<QueryBuilderV2
|
||||||
isListViewPanel={
|
isListViewPanel={isListViewPanel}
|
||||||
panelTypes === PANEL_TYPES.LIST || panelTypes === PANEL_TYPES.TRACE
|
showTraceOperator
|
||||||
}
|
|
||||||
config={{ initialDataSource: DataSource.TRACES, queryVariant: 'static' }}
|
config={{ initialDataSource: DataSource.TRACES, queryVariant: 'static' }}
|
||||||
queryComponents={queryComponents}
|
queryComponents={queryComponents}
|
||||||
panelType={panelTypes}
|
panelType={panelTypes}
|
||||||
|
|||||||
@@ -54,9 +54,11 @@ export const useQueryOperations: UseQueryOperations = ({
|
|||||||
formula,
|
formula,
|
||||||
isListViewPanel = false,
|
isListViewPanel = false,
|
||||||
entityVersion,
|
entityVersion,
|
||||||
|
isForTraceOperator = false,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
handleSetQueryData,
|
handleSetQueryData,
|
||||||
|
handleSetTraceOperatorData,
|
||||||
handleSetFormulaData,
|
handleSetFormulaData,
|
||||||
removeQueryBuilderEntityByIndex,
|
removeQueryBuilderEntityByIndex,
|
||||||
panelType,
|
panelType,
|
||||||
@@ -400,9 +402,19 @@ export const useQueryOperations: UseQueryOperations = ({
|
|||||||
: value,
|
: value,
|
||||||
};
|
};
|
||||||
|
|
||||||
handleSetQueryData(index, newQuery);
|
if (isForTraceOperator) {
|
||||||
|
handleSetTraceOperatorData(index, newQuery);
|
||||||
|
} else {
|
||||||
|
handleSetQueryData(index, newQuery);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[query, index, handleSetQueryData],
|
[
|
||||||
|
query,
|
||||||
|
index,
|
||||||
|
handleSetQueryData,
|
||||||
|
handleSetTraceOperatorData,
|
||||||
|
isForTraceOperator,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleChangeFormulaData: HandleChangeFormulaData = useCallback(
|
const handleChangeFormulaData: HandleChangeFormulaData = useCallback(
|
||||||
|
|||||||
@@ -53,7 +53,6 @@ function TracesExplorer(): JSX.Element {
|
|||||||
handleRunQuery,
|
handleRunQuery,
|
||||||
stagedQuery,
|
stagedQuery,
|
||||||
handleSetConfig,
|
handleSetConfig,
|
||||||
updateQueriesData,
|
|
||||||
} = useQueryBuilder();
|
} = useQueryBuilder();
|
||||||
|
|
||||||
const { options } = useOptionsMenu({
|
const { options } = useOptionsMenu({
|
||||||
@@ -112,48 +111,14 @@ function TracesExplorer(): JSX.Element {
|
|||||||
handleSetConfig(PANEL_TYPES.LIST, DataSource.TRACES);
|
handleSetConfig(PANEL_TYPES.LIST, DataSource.TRACES);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (view === ExplorerViews.LIST) {
|
// TODO: remove formula when switching to List view
|
||||||
if (
|
|
||||||
selectedView !== ExplorerViews.LIST &&
|
|
||||||
currentQuery?.builder?.queryData?.[0]
|
|
||||||
) {
|
|
||||||
const filterToRetain = currentQuery.builder.queryData[0].filter;
|
|
||||||
|
|
||||||
const newDefaultQuery = updateAllQueriesOperators(
|
|
||||||
initialQueriesMap.traces,
|
|
||||||
PANEL_TYPES.LIST,
|
|
||||||
DataSource.TRACES,
|
|
||||||
);
|
|
||||||
|
|
||||||
const newListQuery = updateQueriesData(
|
|
||||||
newDefaultQuery,
|
|
||||||
'queryData',
|
|
||||||
(item, index) => {
|
|
||||||
if (index === 0) {
|
|
||||||
return { ...item, filter: filterToRetain };
|
|
||||||
}
|
|
||||||
return item;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
setDefaultQuery(newListQuery);
|
|
||||||
}
|
|
||||||
setShouldReset(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
setSelectedView(view);
|
setSelectedView(view);
|
||||||
handleExplorerTabChange(
|
handleExplorerTabChange(
|
||||||
view === ExplorerViews.TIMESERIES ? PANEL_TYPES.TIME_SERIES : view,
|
view === ExplorerViews.TIMESERIES ? PANEL_TYPES.TIME_SERIES : view,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[
|
[handleSetConfig, handleExplorerTabChange, selectedView, setSelectedView],
|
||||||
handleSetConfig,
|
|
||||||
handleExplorerTabChange,
|
|
||||||
selectedView,
|
|
||||||
currentQuery,
|
|
||||||
updateAllQueriesOperators,
|
|
||||||
updateQueriesData,
|
|
||||||
setSelectedView,
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const listQuery = useMemo(() => {
|
const listQuery = useMemo(() => {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
initialClickHouseData,
|
initialClickHouseData,
|
||||||
initialFormulaBuilderFormValues,
|
initialFormulaBuilderFormValues,
|
||||||
initialQueriesMap,
|
initialQueriesMap,
|
||||||
|
initialQueryBuilderFormTraceOperatorValues,
|
||||||
initialQueryBuilderFormValuesMap,
|
initialQueryBuilderFormValuesMap,
|
||||||
initialQueryPromQLData,
|
initialQueryPromQLData,
|
||||||
initialQueryState,
|
initialQueryState,
|
||||||
@@ -14,6 +15,7 @@ import {
|
|||||||
MAX_FORMULAS,
|
MAX_FORMULAS,
|
||||||
MAX_QUERIES,
|
MAX_QUERIES,
|
||||||
PANEL_TYPES,
|
PANEL_TYPES,
|
||||||
|
TRACE_OPERATOR_QUERY_NAME,
|
||||||
} from 'constants/queryBuilder';
|
} from 'constants/queryBuilder';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import {
|
import {
|
||||||
@@ -47,6 +49,7 @@ import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteRe
|
|||||||
import {
|
import {
|
||||||
IBuilderFormula,
|
IBuilderFormula,
|
||||||
IBuilderQuery,
|
IBuilderQuery,
|
||||||
|
IBuilderTraceOperator,
|
||||||
IClickHouseQuery,
|
IClickHouseQuery,
|
||||||
IPromQLQuery,
|
IPromQLQuery,
|
||||||
Query,
|
Query,
|
||||||
@@ -75,14 +78,18 @@ export const QueryBuilderContext = createContext<QueryBuilderContextType>({
|
|||||||
panelType: PANEL_TYPES.TIME_SERIES,
|
panelType: PANEL_TYPES.TIME_SERIES,
|
||||||
isEnabledQuery: false,
|
isEnabledQuery: false,
|
||||||
handleSetQueryData: () => {},
|
handleSetQueryData: () => {},
|
||||||
|
handleSetTraceOperatorData: () => {},
|
||||||
handleSetFormulaData: () => {},
|
handleSetFormulaData: () => {},
|
||||||
handleSetQueryItemData: () => {},
|
handleSetQueryItemData: () => {},
|
||||||
handleSetConfig: () => {},
|
handleSetConfig: () => {},
|
||||||
removeQueryBuilderEntityByIndex: () => {},
|
removeQueryBuilderEntityByIndex: () => {},
|
||||||
|
removeAllQueryBuilderEntities: () => {},
|
||||||
removeQueryTypeItemByIndex: () => {},
|
removeQueryTypeItemByIndex: () => {},
|
||||||
addNewBuilderQuery: () => {},
|
addNewBuilderQuery: () => {},
|
||||||
cloneQuery: () => {},
|
cloneQuery: () => {},
|
||||||
addNewFormula: () => {},
|
addNewFormula: () => {},
|
||||||
|
addTraceOperator: () => {},
|
||||||
|
removeTraceOperator: () => {},
|
||||||
addNewQueryItem: () => {},
|
addNewQueryItem: () => {},
|
||||||
redirectWithQueryBuilderData: () => {},
|
redirectWithQueryBuilderData: () => {},
|
||||||
handleRunQuery: () => {},
|
handleRunQuery: () => {},
|
||||||
@@ -173,6 +180,10 @@ export function QueryBuilderProvider({
|
|||||||
...initialFormulaBuilderFormValues,
|
...initialFormulaBuilderFormValues,
|
||||||
...item,
|
...item,
|
||||||
})),
|
})),
|
||||||
|
queryTraceOperator: query.builder.queryTraceOperator?.map((item) => ({
|
||||||
|
...initialQueryBuilderFormTraceOperatorValues,
|
||||||
|
...item,
|
||||||
|
})),
|
||||||
};
|
};
|
||||||
|
|
||||||
const setupedQueryData = builder.queryData.map((item) => {
|
const setupedQueryData = builder.queryData.map((item) => {
|
||||||
@@ -385,8 +396,11 @@ export function QueryBuilderProvider({
|
|||||||
const removeQueryBuilderEntityByIndex = useCallback(
|
const removeQueryBuilderEntityByIndex = useCallback(
|
||||||
(type: keyof QueryBuilderData, index: number) => {
|
(type: keyof QueryBuilderData, index: number) => {
|
||||||
setCurrentQuery((prevState) => {
|
setCurrentQuery((prevState) => {
|
||||||
const currentArray: (IBuilderQuery | IBuilderFormula)[] =
|
const currentArray: (
|
||||||
prevState.builder[type];
|
| IBuilderQuery
|
||||||
|
| IBuilderFormula
|
||||||
|
| IBuilderTraceOperator
|
||||||
|
)[] = prevState.builder[type];
|
||||||
|
|
||||||
const filteredArray = currentArray.filter((_, i) => index !== i);
|
const filteredArray = currentArray.filter((_, i) => index !== i);
|
||||||
|
|
||||||
@@ -400,8 +414,11 @@ export function QueryBuilderProvider({
|
|||||||
});
|
});
|
||||||
// eslint-disable-next-line sonarjs/no-identical-functions
|
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||||
setSupersetQuery((prevState) => {
|
setSupersetQuery((prevState) => {
|
||||||
const currentArray: (IBuilderQuery | IBuilderFormula)[] =
|
const currentArray: (
|
||||||
prevState.builder[type];
|
| IBuilderQuery
|
||||||
|
| IBuilderFormula
|
||||||
|
| IBuilderTraceOperator
|
||||||
|
)[] = prevState.builder[type];
|
||||||
|
|
||||||
const filteredArray = currentArray.filter((_, i) => index !== i);
|
const filteredArray = currentArray.filter((_, i) => index !== i);
|
||||||
|
|
||||||
@@ -417,6 +434,20 @@ export function QueryBuilderProvider({
|
|||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const removeAllQueryBuilderEntities = useCallback(
|
||||||
|
(type: keyof QueryBuilderData) => {
|
||||||
|
setCurrentQuery((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
builder: { ...prevState.builder, [type]: [] },
|
||||||
|
}));
|
||||||
|
setSupersetQuery((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
builder: { ...prevState.builder, [type]: [] },
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
[setCurrentQuery, setSupersetQuery],
|
||||||
|
);
|
||||||
|
|
||||||
const removeQueryTypeItemByIndex = useCallback(
|
const removeQueryTypeItemByIndex = useCallback(
|
||||||
(type: EQueryType.PROM | EQueryType.CLICKHOUSE, index: number) => {
|
(type: EQueryType.PROM | EQueryType.CLICKHOUSE, index: number) => {
|
||||||
setCurrentQuery((prevState) => {
|
setCurrentQuery((prevState) => {
|
||||||
@@ -639,6 +670,68 @@ export function QueryBuilderProvider({
|
|||||||
});
|
});
|
||||||
}, [createNewBuilderFormula]);
|
}, [createNewBuilderFormula]);
|
||||||
|
|
||||||
|
const addTraceOperator = useCallback((expression = '') => {
|
||||||
|
const trimmed = (expression || '').trim();
|
||||||
|
|
||||||
|
setCurrentQuery((prevState) => {
|
||||||
|
const existing = prevState.builder.queryTraceOperator?.[0] || null;
|
||||||
|
const updated: IBuilderTraceOperator = existing
|
||||||
|
? { ...existing, expression: trimmed }
|
||||||
|
: {
|
||||||
|
...initialQueryBuilderFormTraceOperatorValues,
|
||||||
|
queryName: TRACE_OPERATOR_QUERY_NAME,
|
||||||
|
expression: trimmed,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...prevState,
|
||||||
|
builder: {
|
||||||
|
...prevState.builder,
|
||||||
|
// enforce single trace operator and replace only expression
|
||||||
|
queryTraceOperator: [updated],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||||
|
setSupersetQuery((prevState) => {
|
||||||
|
const existing = prevState.builder.queryTraceOperator?.[0] || null;
|
||||||
|
const updated: IBuilderTraceOperator = existing
|
||||||
|
? { ...existing, expression: trimmed }
|
||||||
|
: {
|
||||||
|
...initialQueryBuilderFormTraceOperatorValues,
|
||||||
|
queryName: TRACE_OPERATOR_QUERY_NAME,
|
||||||
|
expression: trimmed,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...prevState,
|
||||||
|
builder: {
|
||||||
|
...prevState.builder,
|
||||||
|
// enforce single trace operator and replace only expression
|
||||||
|
queryTraceOperator: [updated],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const removeTraceOperator = useCallback(() => {
|
||||||
|
setCurrentQuery((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
builder: {
|
||||||
|
...prevState.builder,
|
||||||
|
queryTraceOperator: [],
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||||
|
setSupersetQuery((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
builder: {
|
||||||
|
...prevState.builder,
|
||||||
|
queryTraceOperator: [],
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}, []);
|
||||||
|
|
||||||
const updateQueryBuilderData: <T>(
|
const updateQueryBuilderData: <T>(
|
||||||
arr: T[],
|
arr: T[],
|
||||||
index: number,
|
index: number,
|
||||||
@@ -745,6 +838,44 @@ export function QueryBuilderProvider({
|
|||||||
},
|
},
|
||||||
[updateQueryBuilderData, updateSuperSetQueryBuilderData],
|
[updateQueryBuilderData, updateSuperSetQueryBuilderData],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleSetTraceOperatorData = useCallback(
|
||||||
|
(index: number, traceOperatorData: IBuilderTraceOperator): void => {
|
||||||
|
setCurrentQuery((prevState) => {
|
||||||
|
const updatedTraceOperatorBuilderData = updateQueryBuilderData(
|
||||||
|
prevState.builder.queryTraceOperator,
|
||||||
|
index,
|
||||||
|
traceOperatorData,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...prevState,
|
||||||
|
builder: {
|
||||||
|
...prevState.builder,
|
||||||
|
queryTraceOperator: updatedTraceOperatorBuilderData,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||||
|
setSupersetQuery((prevState) => {
|
||||||
|
const updatedTraceOperatorBuilderData = updateQueryBuilderData(
|
||||||
|
prevState.builder.queryTraceOperator,
|
||||||
|
index,
|
||||||
|
traceOperatorData,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...prevState,
|
||||||
|
builder: {
|
||||||
|
...prevState.builder,
|
||||||
|
queryTraceOperator: updatedTraceOperatorBuilderData,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[updateQueryBuilderData],
|
||||||
|
);
|
||||||
|
|
||||||
const handleSetFormulaData = useCallback(
|
const handleSetFormulaData = useCallback(
|
||||||
(index: number, formulaData: IBuilderFormula): void => {
|
(index: number, formulaData: IBuilderFormula): void => {
|
||||||
setCurrentQuery((prevState) => {
|
setCurrentQuery((prevState) => {
|
||||||
@@ -1045,14 +1176,18 @@ export function QueryBuilderProvider({
|
|||||||
panelType,
|
panelType,
|
||||||
isEnabledQuery,
|
isEnabledQuery,
|
||||||
handleSetQueryData,
|
handleSetQueryData,
|
||||||
|
handleSetTraceOperatorData,
|
||||||
handleSetFormulaData,
|
handleSetFormulaData,
|
||||||
handleSetQueryItemData,
|
handleSetQueryItemData,
|
||||||
handleSetConfig,
|
handleSetConfig,
|
||||||
removeQueryBuilderEntityByIndex,
|
removeQueryBuilderEntityByIndex,
|
||||||
removeQueryTypeItemByIndex,
|
removeQueryTypeItemByIndex,
|
||||||
|
removeAllQueryBuilderEntities,
|
||||||
cloneQuery,
|
cloneQuery,
|
||||||
addNewBuilderQuery,
|
addNewBuilderQuery,
|
||||||
addNewFormula,
|
addNewFormula,
|
||||||
|
addTraceOperator,
|
||||||
|
removeTraceOperator,
|
||||||
addNewQueryItem,
|
addNewQueryItem,
|
||||||
redirectWithQueryBuilderData,
|
redirectWithQueryBuilderData,
|
||||||
handleRunQuery,
|
handleRunQuery,
|
||||||
@@ -1073,14 +1208,18 @@ export function QueryBuilderProvider({
|
|||||||
panelType,
|
panelType,
|
||||||
isEnabledQuery,
|
isEnabledQuery,
|
||||||
handleSetQueryData,
|
handleSetQueryData,
|
||||||
|
handleSetTraceOperatorData,
|
||||||
handleSetFormulaData,
|
handleSetFormulaData,
|
||||||
handleSetQueryItemData,
|
handleSetQueryItemData,
|
||||||
handleSetConfig,
|
handleSetConfig,
|
||||||
removeQueryBuilderEntityByIndex,
|
removeQueryBuilderEntityByIndex,
|
||||||
removeQueryTypeItemByIndex,
|
removeQueryTypeItemByIndex,
|
||||||
|
removeAllQueryBuilderEntities,
|
||||||
cloneQuery,
|
cloneQuery,
|
||||||
addNewBuilderQuery,
|
addNewBuilderQuery,
|
||||||
addNewFormula,
|
addNewFormula,
|
||||||
|
addTraceOperator,
|
||||||
|
removeTraceOperator,
|
||||||
addNewQueryItem,
|
addNewQueryItem,
|
||||||
redirectWithQueryBuilderData,
|
redirectWithQueryBuilderData,
|
||||||
handleRunQuery,
|
handleRunQuery,
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ export interface IBuilderFormula {
|
|||||||
orderBy?: OrderByPayload[];
|
orderBy?: OrderByPayload[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type IBuilderTraceOperator = IBuilderQuery & {
|
||||||
|
returnSpansFrom?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export interface TagFilterItem {
|
export interface TagFilterItem {
|
||||||
id: string;
|
id: string;
|
||||||
key?: BaseAutocompleteData;
|
key?: BaseAutocompleteData;
|
||||||
@@ -124,6 +128,7 @@ export type BuilderQueryDataResourse = Record<
|
|||||||
export type MapData =
|
export type MapData =
|
||||||
| IBuilderQuery
|
| IBuilderQuery
|
||||||
| IBuilderFormula
|
| IBuilderFormula
|
||||||
|
| IBuilderTraceOperator
|
||||||
| IClickHouseQuery
|
| IClickHouseQuery
|
||||||
| IPromQLQuery;
|
| IPromQLQuery;
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export type RequestType =
|
|||||||
|
|
||||||
export type QueryType =
|
export type QueryType =
|
||||||
| 'builder_query'
|
| 'builder_query'
|
||||||
|
| 'builder_trace_operator'
|
||||||
| 'builder_formula'
|
| 'builder_formula'
|
||||||
| 'builder_sub_query'
|
| 'builder_sub_query'
|
||||||
| 'builder_join'
|
| 'builder_join'
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteRe
|
|||||||
import {
|
import {
|
||||||
IBuilderFormula,
|
IBuilderFormula,
|
||||||
IBuilderQuery,
|
IBuilderQuery,
|
||||||
|
IBuilderTraceOperator,
|
||||||
} from 'types/api/queryBuilder/queryBuilderData';
|
} from 'types/api/queryBuilder/queryBuilderData';
|
||||||
import {
|
import {
|
||||||
BaseBuilderQuery,
|
BaseBuilderQuery,
|
||||||
@@ -18,6 +19,7 @@ import { SelectOption } from './select';
|
|||||||
|
|
||||||
type UseQueryOperationsParams = Pick<QueryProps, 'index' | 'query'> &
|
type UseQueryOperationsParams = Pick<QueryProps, 'index' | 'query'> &
|
||||||
Pick<QueryBuilderProps, 'filterConfigs'> & {
|
Pick<QueryBuilderProps, 'filterConfigs'> & {
|
||||||
|
isForTraceOperator?: boolean;
|
||||||
formula?: IBuilderFormula;
|
formula?: IBuilderFormula;
|
||||||
isListViewPanel?: boolean;
|
isListViewPanel?: boolean;
|
||||||
entityVersion: string;
|
entityVersion: string;
|
||||||
@@ -32,6 +34,14 @@ export type HandleChangeQueryData<T = IBuilderQuery> = <
|
|||||||
value: Value,
|
value: Value,
|
||||||
) => void;
|
) => void;
|
||||||
|
|
||||||
|
export type HandleChangeTraceOperatorData<T = IBuilderTraceOperator> = <
|
||||||
|
Key extends keyof T,
|
||||||
|
Value extends T[Key]
|
||||||
|
>(
|
||||||
|
key: Key,
|
||||||
|
value: Value,
|
||||||
|
) => void;
|
||||||
|
|
||||||
// Legacy version for backward compatibility
|
// Legacy version for backward compatibility
|
||||||
export type HandleChangeQueryDataLegacy = HandleChangeQueryData<IBuilderQuery>;
|
export type HandleChangeQueryDataLegacy = HandleChangeQueryData<IBuilderQuery>;
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { Dispatch, SetStateAction } from 'react';
|
|||||||
import {
|
import {
|
||||||
IBuilderFormula,
|
IBuilderFormula,
|
||||||
IBuilderQuery,
|
IBuilderQuery,
|
||||||
|
IBuilderTraceOperator,
|
||||||
IClickHouseQuery,
|
IClickHouseQuery,
|
||||||
IPromQLQuery,
|
IPromQLQuery,
|
||||||
Query,
|
Query,
|
||||||
@@ -222,6 +223,7 @@ export type ReduceOperators = 'last' | 'sum' | 'avg' | 'max' | 'min';
|
|||||||
export type QueryBuilderData = {
|
export type QueryBuilderData = {
|
||||||
queryData: IBuilderQuery[];
|
queryData: IBuilderQuery[];
|
||||||
queryFormulas: IBuilderFormula[];
|
queryFormulas: IBuilderFormula[];
|
||||||
|
queryTraceOperator: IBuilderTraceOperator[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type QueryBuilderContextType = {
|
export type QueryBuilderContextType = {
|
||||||
@@ -235,6 +237,10 @@ export type QueryBuilderContextType = {
|
|||||||
panelType: PANEL_TYPES | null;
|
panelType: PANEL_TYPES | null;
|
||||||
isEnabledQuery: boolean;
|
isEnabledQuery: boolean;
|
||||||
handleSetQueryData: (index: number, queryData: IBuilderQuery) => void;
|
handleSetQueryData: (index: number, queryData: IBuilderQuery) => void;
|
||||||
|
handleSetTraceOperatorData: (
|
||||||
|
index: number,
|
||||||
|
traceOperatorData: IBuilderTraceOperator,
|
||||||
|
) => void;
|
||||||
handleSetFormulaData: (index: number, formulaData: IBuilderFormula) => void;
|
handleSetFormulaData: (index: number, formulaData: IBuilderFormula) => void;
|
||||||
handleSetQueryItemData: (
|
handleSetQueryItemData: (
|
||||||
index: number,
|
index: number,
|
||||||
@@ -249,12 +255,15 @@ export type QueryBuilderContextType = {
|
|||||||
type: keyof QueryBuilderData,
|
type: keyof QueryBuilderData,
|
||||||
index: number,
|
index: number,
|
||||||
) => void;
|
) => void;
|
||||||
|
removeAllQueryBuilderEntities: (type: keyof QueryBuilderData) => void;
|
||||||
removeQueryTypeItemByIndex: (
|
removeQueryTypeItemByIndex: (
|
||||||
type: EQueryType.PROM | EQueryType.CLICKHOUSE,
|
type: EQueryType.PROM | EQueryType.CLICKHOUSE,
|
||||||
index: number,
|
index: number,
|
||||||
) => void;
|
) => void;
|
||||||
addNewBuilderQuery: () => void;
|
addNewBuilderQuery: () => void;
|
||||||
addNewFormula: () => void;
|
addNewFormula: () => void;
|
||||||
|
removeTraceOperator: () => void;
|
||||||
|
addTraceOperator: (expression?: string) => void;
|
||||||
cloneQuery: (type: string, query: IBuilderQuery) => void;
|
cloneQuery: (type: string, query: IBuilderQuery) => void;
|
||||||
addNewQueryItem: (type: EQueryType.PROM | EQueryType.CLICKHOUSE) => void;
|
addNewQueryItem: (type: EQueryType.PROM | EQueryType.CLICKHOUSE) => void;
|
||||||
redirectWithQueryBuilderData: (
|
redirectWithQueryBuilderData: (
|
||||||
|
|||||||
@@ -42,8 +42,10 @@ func consume(rows driver.Rows, kind qbtypes.RequestType, queryWindow *qbtypes.Ti
|
|||||||
payload, err = readAsTimeSeries(rows, queryWindow, step, queryName)
|
payload, err = readAsTimeSeries(rows, queryWindow, step, queryName)
|
||||||
case qbtypes.RequestTypeScalar:
|
case qbtypes.RequestTypeScalar:
|
||||||
payload, err = readAsScalar(rows, queryName)
|
payload, err = readAsScalar(rows, queryName)
|
||||||
case qbtypes.RequestTypeRaw, qbtypes.RequestTypeTrace:
|
case qbtypes.RequestTypeRaw:
|
||||||
payload, err = readAsRaw(rows, queryName)
|
payload, err = readAsRaw(rows, queryName)
|
||||||
|
case qbtypes.RequestTypeTrace:
|
||||||
|
payload, err = readAsTrace(rows, queryName)
|
||||||
// TODO: add support for other request types
|
// TODO: add support for other request types
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -332,6 +334,74 @@ func readAsScalar(rows driver.Rows, queryName string) (*qbtypes.ScalarData, erro
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func readAsTrace(rows driver.Rows, queryName string) (*qbtypes.RawData, error) {
|
||||||
|
colNames := rows.Columns()
|
||||||
|
colTypes := rows.ColumnTypes()
|
||||||
|
colCnt := len(colNames)
|
||||||
|
|
||||||
|
scanTpl := make([]any, colCnt)
|
||||||
|
for i, ct := range colTypes {
|
||||||
|
scanTpl[i] = reflect.New(ct.ScanType()).Interface()
|
||||||
|
}
|
||||||
|
|
||||||
|
var outRows []*qbtypes.RawRow
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
scan := make([]any, colCnt)
|
||||||
|
for i := range scanTpl {
|
||||||
|
scan[i] = reflect.New(colTypes[i].ScanType()).Interface()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := rows.Scan(scan...); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rr := qbtypes.RawRow{
|
||||||
|
Data: make(map[string]any, colCnt),
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, cellPtr := range scan {
|
||||||
|
name := colNames[i]
|
||||||
|
|
||||||
|
val := reflect.ValueOf(cellPtr).Elem().Interface()
|
||||||
|
|
||||||
|
if name == "timestamp" || name == "timestamp_datetime" {
|
||||||
|
switch t := val.(type) {
|
||||||
|
case time.Time:
|
||||||
|
rr.Timestamp = t
|
||||||
|
case uint64: // epoch-ns stored as integer
|
||||||
|
rr.Timestamp = time.Unix(0, int64(t))
|
||||||
|
case int64:
|
||||||
|
rr.Timestamp = time.Unix(0, t)
|
||||||
|
case string: // Handle timestamp strings (ISO format)
|
||||||
|
if parsedTime, err := time.Parse(time.RFC3339, t); err == nil {
|
||||||
|
rr.Timestamp = parsedTime
|
||||||
|
} else if parsedTime, err := time.Parse("2006-01-02T15:04:05.999999999Z", t); err == nil {
|
||||||
|
rr.Timestamp = parsedTime
|
||||||
|
} else {
|
||||||
|
// leave zero time if unrecognised
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// leave zero time if unrecognised
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// store value in map as *any, to match the schema
|
||||||
|
v := any(val)
|
||||||
|
rr.Data[name] = &v
|
||||||
|
}
|
||||||
|
outRows = append(outRows, &rr)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &qbtypes.RawData{
|
||||||
|
QueryName: queryName,
|
||||||
|
Rows: outRows,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func derefValue(v any) any {
|
func derefValue(v any) any {
|
||||||
if v == nil {
|
if v == nil {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ func getqueryInfo(spec any) queryInfo {
|
|||||||
return queryInfo{Name: s.Name, Disabled: s.Disabled, Step: s.StepInterval}
|
return queryInfo{Name: s.Name, Disabled: s.Disabled, Step: s.StepInterval}
|
||||||
case qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]:
|
case qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]:
|
||||||
return queryInfo{Name: s.Name, Disabled: s.Disabled, Step: s.StepInterval}
|
return queryInfo{Name: s.Name, Disabled: s.Disabled, Step: s.StepInterval}
|
||||||
|
case qbtypes.QueryBuilderTraceOperator:
|
||||||
|
return queryInfo{Name: s.Name, Disabled: s.Disabled, Step: s.StepInterval}
|
||||||
case qbtypes.QueryBuilderFormula:
|
case qbtypes.QueryBuilderFormula:
|
||||||
return queryInfo{Name: s.Name, Disabled: s.Disabled}
|
return queryInfo{Name: s.Name, Disabled: s.Disabled}
|
||||||
case qbtypes.PromQuery:
|
case qbtypes.PromQuery:
|
||||||
@@ -70,6 +72,10 @@ func (q *querier) postProcessResults(ctx context.Context, results map[string]any
|
|||||||
result = postProcessMetricQuery(q, result, spec, req)
|
result = postProcessMetricQuery(q, result, spec, req)
|
||||||
typedResults[spec.Name] = result
|
typedResults[spec.Name] = result
|
||||||
}
|
}
|
||||||
|
case qbtypes.QueryBuilderTraceOperator:
|
||||||
|
if result, ok := typedResults[spec.Name]; ok {
|
||||||
|
typedResults[spec.Name] = result
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,15 +28,16 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type querier struct {
|
type querier struct {
|
||||||
logger *slog.Logger
|
logger *slog.Logger
|
||||||
telemetryStore telemetrystore.TelemetryStore
|
telemetryStore telemetrystore.TelemetryStore
|
||||||
metadataStore telemetrytypes.MetadataStore
|
metadataStore telemetrytypes.MetadataStore
|
||||||
promEngine prometheus.Prometheus
|
promEngine prometheus.Prometheus
|
||||||
traceStmtBuilder qbtypes.StatementBuilder[qbtypes.TraceAggregation]
|
traceStmtBuilder qbtypes.StatementBuilder[qbtypes.TraceAggregation]
|
||||||
logStmtBuilder qbtypes.StatementBuilder[qbtypes.LogAggregation]
|
logStmtBuilder qbtypes.StatementBuilder[qbtypes.LogAggregation]
|
||||||
metricStmtBuilder qbtypes.StatementBuilder[qbtypes.MetricAggregation]
|
metricStmtBuilder qbtypes.StatementBuilder[qbtypes.MetricAggregation]
|
||||||
meterStmtBuilder qbtypes.StatementBuilder[qbtypes.MetricAggregation]
|
meterStmtBuilder qbtypes.StatementBuilder[qbtypes.MetricAggregation]
|
||||||
bucketCache BucketCache
|
traceOperatorStmtBuilder qbtypes.TraceOperatorStatementBuilder
|
||||||
|
bucketCache BucketCache
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ Querier = (*querier)(nil)
|
var _ Querier = (*querier)(nil)
|
||||||
@@ -50,19 +51,21 @@ func New(
|
|||||||
logStmtBuilder qbtypes.StatementBuilder[qbtypes.LogAggregation],
|
logStmtBuilder qbtypes.StatementBuilder[qbtypes.LogAggregation],
|
||||||
metricStmtBuilder qbtypes.StatementBuilder[qbtypes.MetricAggregation],
|
metricStmtBuilder qbtypes.StatementBuilder[qbtypes.MetricAggregation],
|
||||||
meterStmtBuilder qbtypes.StatementBuilder[qbtypes.MetricAggregation],
|
meterStmtBuilder qbtypes.StatementBuilder[qbtypes.MetricAggregation],
|
||||||
|
traceOperatorStmtBuilder qbtypes.TraceOperatorStatementBuilder,
|
||||||
bucketCache BucketCache,
|
bucketCache BucketCache,
|
||||||
) *querier {
|
) *querier {
|
||||||
querierSettings := factory.NewScopedProviderSettings(settings, "github.com/SigNoz/signoz/pkg/querier")
|
querierSettings := factory.NewScopedProviderSettings(settings, "github.com/SigNoz/signoz/pkg/querier")
|
||||||
return &querier{
|
return &querier{
|
||||||
logger: querierSettings.Logger(),
|
logger: querierSettings.Logger(),
|
||||||
telemetryStore: telemetryStore,
|
telemetryStore: telemetryStore,
|
||||||
metadataStore: metadataStore,
|
metadataStore: metadataStore,
|
||||||
promEngine: promEngine,
|
promEngine: promEngine,
|
||||||
traceStmtBuilder: traceStmtBuilder,
|
traceStmtBuilder: traceStmtBuilder,
|
||||||
logStmtBuilder: logStmtBuilder,
|
logStmtBuilder: logStmtBuilder,
|
||||||
metricStmtBuilder: metricStmtBuilder,
|
metricStmtBuilder: metricStmtBuilder,
|
||||||
meterStmtBuilder: meterStmtBuilder,
|
meterStmtBuilder: meterStmtBuilder,
|
||||||
bucketCache: bucketCache,
|
traceOperatorStmtBuilder: traceOperatorStmtBuilder,
|
||||||
|
bucketCache: bucketCache,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,9 +127,28 @@ func (q *querier) QueryRange(ctx context.Context, orgID valuer.UUID, req *qbtype
|
|||||||
NumberOfQueries: len(req.CompositeQuery.Queries),
|
NumberOfQueries: len(req.CompositeQuery.Queries),
|
||||||
PanelType: req.RequestType.StringValue(),
|
PanelType: req.RequestType.StringValue(),
|
||||||
}
|
}
|
||||||
|
|
||||||
intervalWarnings := []string{}
|
intervalWarnings := []string{}
|
||||||
|
|
||||||
|
dependencyQueries := make(map[string]bool)
|
||||||
|
traceOperatorQueries := make(map[string]qbtypes.QueryBuilderTraceOperator)
|
||||||
|
|
||||||
|
for _, query := range req.CompositeQuery.Queries {
|
||||||
|
if query.Type == qbtypes.QueryTypeTraceOperator {
|
||||||
|
if spec, ok := query.Spec.(qbtypes.QueryBuilderTraceOperator); ok {
|
||||||
|
// Parse expression to find dependencies
|
||||||
|
if err := spec.ParseExpression(); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse trace operator expression: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
deps := spec.CollectReferencedQueries(spec.ParsedExpression)
|
||||||
|
for _, dep := range deps {
|
||||||
|
dependencyQueries[dep] = true
|
||||||
|
}
|
||||||
|
traceOperatorQueries[spec.Name] = spec
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// First pass: collect all metric names that need temporality
|
// First pass: collect all metric names that need temporality
|
||||||
metricNames := make([]string, 0)
|
metricNames := make([]string, 0)
|
||||||
for idx, query := range req.CompositeQuery.Queries {
|
for idx, query := range req.CompositeQuery.Queries {
|
||||||
@@ -220,6 +242,21 @@ func (q *querier) QueryRange(ctx context.Context, orgID valuer.UUID, req *qbtype
|
|||||||
event.TracesUsed = strings.Contains(spec.Query, "signoz_traces")
|
event.TracesUsed = strings.Contains(spec.Query, "signoz_traces")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if query.Type == qbtypes.QueryTypeTraceOperator {
|
||||||
|
if spec, ok := query.Spec.(qbtypes.QueryBuilderTraceOperator); ok {
|
||||||
|
if spec.StepInterval.Seconds() == 0 {
|
||||||
|
spec.StepInterval = qbtypes.Step{
|
||||||
|
Duration: time.Second * time.Duration(querybuilder.RecommendedStepInterval(req.Start, req.End)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if spec.StepInterval.Seconds() < float64(querybuilder.MinAllowedStepInterval(req.Start, req.End)) {
|
||||||
|
spec.StepInterval = qbtypes.Step{
|
||||||
|
Duration: time.Second * time.Duration(querybuilder.MinAllowedStepInterval(req.Start, req.End)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
req.CompositeQuery.Queries[idx].Spec = spec
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -240,6 +277,38 @@ func (q *querier) QueryRange(ctx context.Context, orgID valuer.UUID, req *qbtype
|
|||||||
steps := make(map[string]qbtypes.Step)
|
steps := make(map[string]qbtypes.Step)
|
||||||
|
|
||||||
for _, query := range req.CompositeQuery.Queries {
|
for _, query := range req.CompositeQuery.Queries {
|
||||||
|
var queryName string
|
||||||
|
var isTraceOperator bool
|
||||||
|
|
||||||
|
switch query.Type {
|
||||||
|
case qbtypes.QueryTypeTraceOperator:
|
||||||
|
if spec, ok := query.Spec.(qbtypes.QueryBuilderTraceOperator); ok {
|
||||||
|
queryName = spec.Name
|
||||||
|
isTraceOperator = true
|
||||||
|
}
|
||||||
|
case qbtypes.QueryTypePromQL:
|
||||||
|
if spec, ok := query.Spec.(qbtypes.PromQuery); ok {
|
||||||
|
queryName = spec.Name
|
||||||
|
}
|
||||||
|
case qbtypes.QueryTypeClickHouseSQL:
|
||||||
|
if spec, ok := query.Spec.(qbtypes.ClickHouseQuery); ok {
|
||||||
|
queryName = spec.Name
|
||||||
|
}
|
||||||
|
case qbtypes.QueryTypeBuilder:
|
||||||
|
switch spec := query.Spec.(type) {
|
||||||
|
case qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]:
|
||||||
|
queryName = spec.Name
|
||||||
|
case qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]:
|
||||||
|
queryName = spec.Name
|
||||||
|
case qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]:
|
||||||
|
queryName = spec.Name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isTraceOperator && dependencyQueries[queryName] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
switch query.Type {
|
switch query.Type {
|
||||||
case qbtypes.QueryTypePromQL:
|
case qbtypes.QueryTypePromQL:
|
||||||
promQuery, ok := query.Spec.(qbtypes.PromQuery)
|
promQuery, ok := query.Spec.(qbtypes.PromQuery)
|
||||||
@@ -256,6 +325,22 @@ func (q *querier) QueryRange(ctx context.Context, orgID valuer.UUID, req *qbtype
|
|||||||
}
|
}
|
||||||
chSQLQuery := newchSQLQuery(q.logger, q.telemetryStore, chQuery, nil, qbtypes.TimeRange{From: req.Start, To: req.End}, req.RequestType, tmplVars)
|
chSQLQuery := newchSQLQuery(q.logger, q.telemetryStore, chQuery, nil, qbtypes.TimeRange{From: req.Start, To: req.End}, req.RequestType, tmplVars)
|
||||||
queries[chQuery.Name] = chSQLQuery
|
queries[chQuery.Name] = chSQLQuery
|
||||||
|
case qbtypes.QueryTypeTraceOperator:
|
||||||
|
traceOpQuery, ok := query.Spec.(qbtypes.QueryBuilderTraceOperator)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid trace operator query spec %T", query.Spec)
|
||||||
|
}
|
||||||
|
toq := &traceOperatorQuery{
|
||||||
|
telemetryStore: q.telemetryStore,
|
||||||
|
stmtBuilder: q.traceOperatorStmtBuilder,
|
||||||
|
spec: traceOpQuery,
|
||||||
|
compositeQuery: &req.CompositeQuery,
|
||||||
|
fromMS: uint64(req.Start),
|
||||||
|
toMS: uint64(req.End),
|
||||||
|
kind: req.RequestType,
|
||||||
|
}
|
||||||
|
queries[traceOpQuery.Name] = toq
|
||||||
|
steps[traceOpQuery.Name] = traceOpQuery.StepInterval
|
||||||
case qbtypes.QueryTypeBuilder:
|
case qbtypes.QueryTypeBuilder:
|
||||||
switch spec := query.Spec.(type) {
|
switch spec := query.Spec.(type) {
|
||||||
case qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]:
|
case qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]:
|
||||||
@@ -578,7 +663,16 @@ func (q *querier) createRangedQuery(originalQuery qbtypes.Query, timeRange qbtyp
|
|||||||
return newBuilderQuery(q.telemetryStore, q.meterStmtBuilder, specCopy, adjustedTimeRange, qt.kind, qt.variables)
|
return newBuilderQuery(q.telemetryStore, q.meterStmtBuilder, specCopy, adjustedTimeRange, qt.kind, qt.variables)
|
||||||
}
|
}
|
||||||
return newBuilderQuery(q.telemetryStore, q.metricStmtBuilder, specCopy, adjustedTimeRange, qt.kind, qt.variables)
|
return newBuilderQuery(q.telemetryStore, q.metricStmtBuilder, specCopy, adjustedTimeRange, qt.kind, qt.variables)
|
||||||
|
case *traceOperatorQuery:
|
||||||
|
return &traceOperatorQuery{
|
||||||
|
telemetryStore: q.telemetryStore,
|
||||||
|
stmtBuilder: q.traceOperatorStmtBuilder,
|
||||||
|
spec: qt.spec,
|
||||||
|
fromMS: uint64(timeRange.From),
|
||||||
|
toMS: uint64(timeRange.To),
|
||||||
|
compositeQuery: qt.compositeQuery,
|
||||||
|
kind: qt.kind,
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,6 +89,16 @@ func newProvider(
|
|||||||
telemetryStore,
|
telemetryStore,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ADD: Create trace operator statement builder
|
||||||
|
traceOperatorStmtBuilder := telemetrytraces.NewTraceOperatorStatementBuilder(
|
||||||
|
settings,
|
||||||
|
telemetryMetadataStore,
|
||||||
|
traceFieldMapper,
|
||||||
|
traceConditionBuilder,
|
||||||
|
traceStmtBuilder, // Pass the regular trace statement builder
|
||||||
|
traceAggExprRewriter,
|
||||||
|
)
|
||||||
|
|
||||||
// Create log statement builder
|
// Create log statement builder
|
||||||
logFieldMapper := telemetrylogs.NewFieldMapper()
|
logFieldMapper := telemetrylogs.NewFieldMapper()
|
||||||
logConditionBuilder := telemetrylogs.NewConditionBuilder(logFieldMapper)
|
logConditionBuilder := telemetrylogs.NewConditionBuilder(logFieldMapper)
|
||||||
@@ -147,7 +157,7 @@ func newProvider(
|
|||||||
cfg.FluxInterval,
|
cfg.FluxInterval,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Create and return the querier
|
// Create and return the querier - ADD traceOperatorStmtBuilder parameter
|
||||||
return querier.New(
|
return querier.New(
|
||||||
settings,
|
settings,
|
||||||
telemetryStore,
|
telemetryStore,
|
||||||
@@ -157,6 +167,7 @@ func newProvider(
|
|||||||
logStmtBuilder,
|
logStmtBuilder,
|
||||||
metricStmtBuilder,
|
metricStmtBuilder,
|
||||||
meterStmtBuilder,
|
meterStmtBuilder,
|
||||||
|
traceOperatorStmtBuilder,
|
||||||
bucketCache,
|
bucketCache,
|
||||||
), nil
|
), nil
|
||||||
}
|
}
|
||||||
|
|||||||
145
pkg/querier/trace_operator_query.go
Normal file
145
pkg/querier/trace_operator_query.go
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
package querier
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ClickHouse/clickhouse-go/v2"
|
||||||
|
"github.com/SigNoz/signoz/pkg/telemetrystore"
|
||||||
|
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||||
|
)
|
||||||
|
|
||||||
|
type traceOperatorQuery struct {
|
||||||
|
telemetryStore telemetrystore.TelemetryStore
|
||||||
|
stmtBuilder qbtypes.TraceOperatorStatementBuilder
|
||||||
|
spec qbtypes.QueryBuilderTraceOperator
|
||||||
|
compositeQuery *qbtypes.CompositeQuery
|
||||||
|
fromMS uint64
|
||||||
|
toMS uint64
|
||||||
|
kind qbtypes.RequestType
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ qbtypes.Query = (*traceOperatorQuery)(nil)
|
||||||
|
|
||||||
|
func (q *traceOperatorQuery) Fingerprint() string {
|
||||||
|
|
||||||
|
if q.kind == qbtypes.RequestTypeRaw {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := []string{"trace_operator"}
|
||||||
|
|
||||||
|
parts = append(parts, fmt.Sprintf("expr=%s", q.spec.Expression))
|
||||||
|
|
||||||
|
// Add returnSpansFrom if specified
|
||||||
|
if q.spec.ReturnSpansFrom != "" {
|
||||||
|
parts = append(parts, fmt.Sprintf("return=%s", q.spec.ReturnSpansFrom))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add step interval if present
|
||||||
|
parts = append(parts, fmt.Sprintf("step=%s", q.spec.StepInterval.String()))
|
||||||
|
|
||||||
|
// Add filter if present
|
||||||
|
if q.spec.Filter != nil && q.spec.Filter.Expression != "" {
|
||||||
|
parts = append(parts, fmt.Sprintf("filter=%s", q.spec.Filter.Expression))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add aggregations
|
||||||
|
if len(q.spec.Aggregations) > 0 {
|
||||||
|
aggParts := []string{}
|
||||||
|
for _, agg := range q.spec.Aggregations {
|
||||||
|
aggParts = append(aggParts, agg.Expression)
|
||||||
|
}
|
||||||
|
parts = append(parts, fmt.Sprintf("aggs=[%s]", strings.Join(aggParts, ",")))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add group by
|
||||||
|
if len(q.spec.GroupBy) > 0 {
|
||||||
|
groupByParts := []string{}
|
||||||
|
for _, gb := range q.spec.GroupBy {
|
||||||
|
groupByParts = append(groupByParts, fingerprintGroupByKey(gb))
|
||||||
|
}
|
||||||
|
parts = append(parts, fmt.Sprintf("groupby=[%s]", strings.Join(groupByParts, ",")))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add order by
|
||||||
|
if len(q.spec.Order) > 0 {
|
||||||
|
orderParts := []string{}
|
||||||
|
for _, o := range q.spec.Order {
|
||||||
|
orderParts = append(orderParts, fingerprintOrderBy(o))
|
||||||
|
}
|
||||||
|
parts = append(parts, fmt.Sprintf("order=[%s]", strings.Join(orderParts, ",")))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add limit
|
||||||
|
if q.spec.Limit > 0 {
|
||||||
|
parts = append(parts, fmt.Sprintf("limit=%d", q.spec.Limit))
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(parts, "&")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *traceOperatorQuery) Window() (uint64, uint64) {
|
||||||
|
return q.fromMS, q.toMS
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *traceOperatorQuery) Execute(ctx context.Context) (*qbtypes.Result, error) {
|
||||||
|
stmt, err := q.stmtBuilder.Build(
|
||||||
|
ctx,
|
||||||
|
q.fromMS,
|
||||||
|
q.toMS,
|
||||||
|
q.kind,
|
||||||
|
q.spec,
|
||||||
|
q.compositeQuery,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute the query with proper context
|
||||||
|
result, err := q.executeWithContext(ctx, stmt.Query, stmt.Args)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result.Warnings = stmt.Warnings
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *traceOperatorQuery) executeWithContext(ctx context.Context, query string, args []any) (*qbtypes.Result, error) {
|
||||||
|
totalRows := uint64(0)
|
||||||
|
totalBytes := uint64(0)
|
||||||
|
elapsed := time.Duration(0)
|
||||||
|
|
||||||
|
ctx = clickhouse.Context(ctx, clickhouse.WithProgress(func(p *clickhouse.Progress) {
|
||||||
|
totalRows += p.Rows
|
||||||
|
totalBytes += p.Bytes
|
||||||
|
elapsed += p.Elapsed
|
||||||
|
}))
|
||||||
|
|
||||||
|
rows, err := q.telemetryStore.ClickhouseDB().Query(ctx, query, args...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
// Pass query window and step for partial value detection
|
||||||
|
queryWindow := &qbtypes.TimeRange{From: q.fromMS, To: q.toMS}
|
||||||
|
|
||||||
|
// Use the consume function like builderQuery does
|
||||||
|
payload, err := consume(rows, q.kind, queryWindow, q.spec.StepInterval, q.spec.Name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &qbtypes.Result{
|
||||||
|
Type: q.kind,
|
||||||
|
Value: payload,
|
||||||
|
Stats: qbtypes.ExecStats{
|
||||||
|
RowsScanned: totalRows,
|
||||||
|
BytesScanned: totalBytes,
|
||||||
|
DurationMS: uint64(elapsed.Milliseconds()),
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
959
pkg/telemetrytraces/trace_operator_cte_builder.go
Normal file
959
pkg/telemetrytraces/trace_operator_cte_builder.go
Normal file
@@ -0,0 +1,959 @@
|
|||||||
|
package telemetrytraces
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/SigNoz/signoz/pkg/errors"
|
||||||
|
"github.com/SigNoz/signoz/pkg/querybuilder"
|
||||||
|
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||||
|
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||||
|
"github.com/huandu/go-sqlbuilder"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type cteNode struct {
|
||||||
|
name string
|
||||||
|
sql string
|
||||||
|
args []any
|
||||||
|
dependsOn []string
|
||||||
|
}
|
||||||
|
|
||||||
|
type traceOperatorCTEBuilder struct {
|
||||||
|
ctx context.Context
|
||||||
|
start uint64
|
||||||
|
end uint64
|
||||||
|
operator *qbtypes.QueryBuilderTraceOperator
|
||||||
|
stmtBuilder *traceOperatorStatementBuilder
|
||||||
|
queries map[string]*qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]
|
||||||
|
ctes []cteNode
|
||||||
|
cteNameToIndex map[string]int
|
||||||
|
queryToCTEName map[string]string
|
||||||
|
compositeQuery *qbtypes.CompositeQuery
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *traceOperatorCTEBuilder) collectQueries() error {
|
||||||
|
referencedQueries := b.operator.CollectReferencedQueries(b.operator.ParsedExpression)
|
||||||
|
|
||||||
|
for _, queryEnv := range b.compositeQuery.Queries {
|
||||||
|
if queryEnv.Type == qbtypes.QueryTypeBuilder {
|
||||||
|
if traceQuery, ok := queryEnv.Spec.(qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]); ok {
|
||||||
|
for _, refName := range referencedQueries {
|
||||||
|
if traceQuery.Name == refName {
|
||||||
|
queryCopy := traceQuery
|
||||||
|
b.queries[refName] = &queryCopy
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, refName := range referencedQueries {
|
||||||
|
if _, found := b.queries[refName]; !found {
|
||||||
|
return errors.NewInvalidInputf(errors.CodeInvalidInput, "referenced query '%s' not found", refName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *traceOperatorCTEBuilder) build(requestType qbtypes.RequestType) (*qbtypes.Statement, error) {
|
||||||
|
if len(b.queries) == 0 {
|
||||||
|
if err := b.collectQueries(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err := b.buildBaseSpansCTE()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rootCTEName, err := b.buildExpressionCTEs(b.operator.ParsedExpression)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
selectFromCTE := rootCTEName
|
||||||
|
if b.operator.ReturnSpansFrom != "" {
|
||||||
|
selectFromCTE = b.queryToCTEName[b.operator.ReturnSpansFrom]
|
||||||
|
if selectFromCTE == "" {
|
||||||
|
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput,
|
||||||
|
"returnSpansFrom references query '%s' which has no corresponding CTE",
|
||||||
|
b.operator.ReturnSpansFrom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
finalStmt, err := b.buildFinalQuery(selectFromCTE, requestType)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var cteFragments []string
|
||||||
|
var cteArgs [][]any
|
||||||
|
|
||||||
|
timeConstantsCTE := b.buildTimeConstantsCTE()
|
||||||
|
cteFragments = append(cteFragments, timeConstantsCTE)
|
||||||
|
|
||||||
|
for _, cte := range b.ctes {
|
||||||
|
cteFragments = append(cteFragments, fmt.Sprintf("%s AS (%s)", cte.name, cte.sql))
|
||||||
|
cteArgs = append(cteArgs, cte.args)
|
||||||
|
}
|
||||||
|
|
||||||
|
finalSQL := querybuilder.CombineCTEs(cteFragments) + finalStmt.Query
|
||||||
|
finalArgs := querybuilder.PrependArgs(cteArgs, finalStmt.Args)
|
||||||
|
|
||||||
|
return &qbtypes.Statement{
|
||||||
|
Query: finalSQL,
|
||||||
|
Args: finalArgs,
|
||||||
|
Warnings: finalStmt.Warnings,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *traceOperatorCTEBuilder) buildTimeConstantsCTE() string {
|
||||||
|
startBucket := b.start/querybuilder.NsToSeconds - querybuilder.BucketAdjustment
|
||||||
|
endBucket := b.end / querybuilder.NsToSeconds
|
||||||
|
|
||||||
|
return fmt.Sprintf(`
|
||||||
|
toDateTime64(%d, 9) AS t_from,
|
||||||
|
toDateTime64(%d, 9) AS t_to,
|
||||||
|
%d AS bucket_from,
|
||||||
|
%d AS bucket_to`,
|
||||||
|
b.start, b.end, startBucket, endBucket)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *traceOperatorCTEBuilder) buildBaseSpansCTE() error {
|
||||||
|
sb := sqlbuilder.NewSelectBuilder()
|
||||||
|
|
||||||
|
sb.Select(
|
||||||
|
"trace_id",
|
||||||
|
"span_id",
|
||||||
|
"parent_span_id",
|
||||||
|
"name",
|
||||||
|
"timestamp",
|
||||||
|
"duration_nano",
|
||||||
|
sqlbuilder.Escape("resource_string_service$$name")+" AS `service.name`",
|
||||||
|
sqlbuilder.Escape("resource_string_service$$name"),
|
||||||
|
sqlbuilder.Escape("resource_string_service$$name_exists"),
|
||||||
|
"attributes_string",
|
||||||
|
"attributes_number",
|
||||||
|
"attributes_bool",
|
||||||
|
"resources_string",
|
||||||
|
)
|
||||||
|
|
||||||
|
sb.From(fmt.Sprintf("%s.%s", DBName, SpanIndexV3TableName))
|
||||||
|
|
||||||
|
startBucket := b.start/querybuilder.NsToSeconds - querybuilder.BucketAdjustment
|
||||||
|
endBucket := b.end / querybuilder.NsToSeconds
|
||||||
|
|
||||||
|
sb.Where(
|
||||||
|
sb.GE("timestamp", fmt.Sprintf("%d", b.start)),
|
||||||
|
sb.L("timestamp", fmt.Sprintf("%d", b.end)),
|
||||||
|
sb.GE("ts_bucket_start", startBucket),
|
||||||
|
sb.LE("ts_bucket_start", endBucket),
|
||||||
|
)
|
||||||
|
|
||||||
|
sql, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||||
|
b.addCTE("base_spans", sql, args, nil)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *traceOperatorCTEBuilder) buildExpressionCTEs(expr *qbtypes.TraceOperand) (string, error) {
|
||||||
|
if expr == nil {
|
||||||
|
return "", errors.NewInvalidInputf(errors.CodeInvalidInput, "expression is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if expr.QueryRef != nil {
|
||||||
|
return b.buildQueryCTE(expr.QueryRef.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
var leftCTE, rightCTE string
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if expr.Left != nil {
|
||||||
|
leftCTE, err = b.buildExpressionCTEs(expr.Left)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if expr.Right != nil {
|
||||||
|
rightCTE, err = b.buildExpressionCTEs(expr.Right)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.buildOperatorCTE(*expr.Operator, leftCTE, rightCTE)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *traceOperatorCTEBuilder) buildQueryCTE(queryName string) (string, error) {
|
||||||
|
query, exists := b.queries[queryName]
|
||||||
|
if !exists {
|
||||||
|
return "", errors.NewInvalidInputf(errors.CodeInvalidInput, "query %s not found", queryName)
|
||||||
|
}
|
||||||
|
|
||||||
|
cteName := queryName
|
||||||
|
b.queryToCTEName[queryName] = cteName
|
||||||
|
|
||||||
|
if _, exists := b.cteNameToIndex[cteName]; exists {
|
||||||
|
return cteName, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
keySelectors := getKeySelectors(*query)
|
||||||
|
keys, _, err := b.stmtBuilder.metadataStore.GetKeysMulti(b.ctx, keySelectors)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
sb := sqlbuilder.NewSelectBuilder()
|
||||||
|
sb.Select(
|
||||||
|
"trace_id",
|
||||||
|
"span_id",
|
||||||
|
"parent_span_id",
|
||||||
|
"name",
|
||||||
|
"timestamp",
|
||||||
|
"duration_nano",
|
||||||
|
"`service.name`",
|
||||||
|
fmt.Sprintf("'%s' AS level", cteName),
|
||||||
|
)
|
||||||
|
|
||||||
|
requiredColumns := b.getRequiredAttributeColumns()
|
||||||
|
for _, col := range requiredColumns {
|
||||||
|
sb.SelectMore(col)
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.From("base_spans AS s")
|
||||||
|
|
||||||
|
if query.Filter != nil && query.Filter.Expression != "" {
|
||||||
|
filterWhereClause, err := querybuilder.PrepareWhereClause(
|
||||||
|
query.Filter.Expression,
|
||||||
|
querybuilder.FilterExprVisitorOpts{
|
||||||
|
Logger: b.stmtBuilder.logger,
|
||||||
|
FieldMapper: b.stmtBuilder.fm,
|
||||||
|
ConditionBuilder: b.stmtBuilder.cb,
|
||||||
|
FieldKeys: keys,
|
||||||
|
SkipResourceFilter: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if filterWhereClause != nil {
|
||||||
|
sb.AddWhereClause(filterWhereClause.WhereClause)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sql, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||||
|
b.addCTE(cteName, sql, args, []string{"base_spans"})
|
||||||
|
|
||||||
|
return cteName, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func sanitizeForSQL(s string) string {
|
||||||
|
replacements := map[string]string{
|
||||||
|
"=>": "DIRECT_DESC",
|
||||||
|
"->": "INDIRECT_DESC",
|
||||||
|
"&&": "AND",
|
||||||
|
"||": "OR",
|
||||||
|
"NOT": "NOT",
|
||||||
|
" ": "_",
|
||||||
|
}
|
||||||
|
|
||||||
|
result := s
|
||||||
|
for old, new := range replacements {
|
||||||
|
result = strings.ReplaceAll(result, old, new)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *traceOperatorCTEBuilder) buildOperatorCTE(op qbtypes.TraceOperatorType, leftCTE, rightCTE string) (string, error) {
|
||||||
|
sanitizedOp := sanitizeForSQL(op.StringValue())
|
||||||
|
cteName := fmt.Sprintf("%s_%s_%s", leftCTE, sanitizedOp, rightCTE)
|
||||||
|
|
||||||
|
if _, exists := b.cteNameToIndex[cteName]; exists {
|
||||||
|
return cteName, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var sql string
|
||||||
|
var args []any
|
||||||
|
var dependsOn []string
|
||||||
|
|
||||||
|
switch op {
|
||||||
|
case qbtypes.TraceOperatorDirectDescendant:
|
||||||
|
sql, args, dependsOn = b.buildDirectDescendantCTE(leftCTE, rightCTE)
|
||||||
|
case qbtypes.TraceOperatorAnd:
|
||||||
|
sql, args, dependsOn = b.buildAndCTE(leftCTE, rightCTE)
|
||||||
|
case qbtypes.TraceOperatorOr:
|
||||||
|
sql, dependsOn = b.buildOrCTE(leftCTE, rightCTE)
|
||||||
|
args = nil
|
||||||
|
case qbtypes.TraceOperatorNot, qbtypes.TraceOperatorExclude:
|
||||||
|
sql, args, dependsOn = b.buildNotCTE(leftCTE, rightCTE)
|
||||||
|
default:
|
||||||
|
return "", errors.NewInvalidInputf(errors.CodeInvalidInput, "unsupported operator: %s", op.StringValue())
|
||||||
|
}
|
||||||
|
|
||||||
|
b.addCTE(cteName, sql, args, dependsOn)
|
||||||
|
return cteName, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *traceOperatorCTEBuilder) buildDirectDescendantCTE(parentCTE, childCTE string) (string, []any, []string) {
|
||||||
|
sb := sqlbuilder.NewSelectBuilder()
|
||||||
|
sb.Select(
|
||||||
|
"c.trace_id",
|
||||||
|
"c.span_id",
|
||||||
|
"c.parent_span_id",
|
||||||
|
"c.name",
|
||||||
|
"c.timestamp",
|
||||||
|
"c.duration_nano",
|
||||||
|
"c.`service.name`",
|
||||||
|
fmt.Sprintf("'%s' AS level", childCTE),
|
||||||
|
)
|
||||||
|
|
||||||
|
requiredColumns := b.getRequiredAttributeColumns()
|
||||||
|
for _, col := range requiredColumns {
|
||||||
|
sb.SelectMore(fmt.Sprintf("c.%s", col))
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.From(fmt.Sprintf("%s AS c", childCTE))
|
||||||
|
sb.JoinWithOption(
|
||||||
|
sqlbuilder.InnerJoin,
|
||||||
|
fmt.Sprintf("%s AS p", parentCTE),
|
||||||
|
"p.trace_id = c.trace_id AND p.span_id = c.parent_span_id",
|
||||||
|
)
|
||||||
|
|
||||||
|
sql, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||||
|
return sql, args, []string{parentCTE, childCTE}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *traceOperatorCTEBuilder) buildAndCTE(leftCTE, rightCTE string) (string, []any, []string) {
|
||||||
|
sb := sqlbuilder.NewSelectBuilder()
|
||||||
|
sb.Select(
|
||||||
|
"l.trace_id",
|
||||||
|
"l.span_id",
|
||||||
|
"l.parent_span_id",
|
||||||
|
"l.name",
|
||||||
|
"l.timestamp",
|
||||||
|
"l.duration_nano",
|
||||||
|
"l.`service.name`",
|
||||||
|
"l.level",
|
||||||
|
)
|
||||||
|
|
||||||
|
requiredColumns := b.getRequiredAttributeColumns()
|
||||||
|
for _, col := range requiredColumns {
|
||||||
|
sb.SelectMore(fmt.Sprintf("l.%s", col))
|
||||||
|
}
|
||||||
|
sb.From(fmt.Sprintf("%s AS l", leftCTE))
|
||||||
|
sb.JoinWithOption(
|
||||||
|
sqlbuilder.InnerJoin,
|
||||||
|
fmt.Sprintf("%s AS r", rightCTE),
|
||||||
|
"l.trace_id = r.trace_id",
|
||||||
|
)
|
||||||
|
|
||||||
|
sql, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||||
|
return sql, args, []string{leftCTE, rightCTE}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *traceOperatorCTEBuilder) buildOrCTE(leftCTE, rightCTE string) (string, []string) {
|
||||||
|
sql := fmt.Sprintf(`
|
||||||
|
SELECT * FROM %s
|
||||||
|
UNION DISTINCT
|
||||||
|
SELECT * FROM %s
|
||||||
|
`, leftCTE, rightCTE)
|
||||||
|
|
||||||
|
return sql, []string{leftCTE, rightCTE}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *traceOperatorCTEBuilder) buildNotCTE(leftCTE, rightCTE string) (string, []any, []string) {
|
||||||
|
sb := sqlbuilder.NewSelectBuilder()
|
||||||
|
sb.Select(
|
||||||
|
"l.trace_id",
|
||||||
|
"l.span_id",
|
||||||
|
"l.parent_span_id",
|
||||||
|
"l.name",
|
||||||
|
"l.timestamp",
|
||||||
|
"l.duration_nano",
|
||||||
|
"l.`service.name`",
|
||||||
|
"l.level",
|
||||||
|
)
|
||||||
|
|
||||||
|
requiredColumns := b.getRequiredAttributeColumns()
|
||||||
|
for _, col := range requiredColumns {
|
||||||
|
sb.SelectMore(fmt.Sprintf("l.%s", col))
|
||||||
|
}
|
||||||
|
sb.From(fmt.Sprintf("%s AS l", leftCTE))
|
||||||
|
sb.Where(fmt.Sprintf(
|
||||||
|
"NOT EXISTS (SELECT 1 FROM %s AS r WHERE r.trace_id = l.trace_id)",
|
||||||
|
rightCTE,
|
||||||
|
))
|
||||||
|
|
||||||
|
sql, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||||
|
return sql, args, []string{leftCTE, rightCTE}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *traceOperatorCTEBuilder) buildFinalQuery(selectFromCTE string, requestType qbtypes.RequestType) (*qbtypes.Statement, error) {
|
||||||
|
switch requestType {
|
||||||
|
case qbtypes.RequestTypeRaw:
|
||||||
|
return b.buildListQuery(selectFromCTE)
|
||||||
|
case qbtypes.RequestTypeTimeSeries:
|
||||||
|
return b.buildTimeSeriesQuery(selectFromCTE)
|
||||||
|
case qbtypes.RequestTypeTrace:
|
||||||
|
return b.buildTraceQuery(selectFromCTE)
|
||||||
|
case qbtypes.RequestTypeScalar:
|
||||||
|
return b.buildScalarQuery(selectFromCTE)
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported request type: %s", requestType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *traceOperatorCTEBuilder) buildListQuery(selectFromCTE string) (*qbtypes.Statement, error) {
|
||||||
|
sb := sqlbuilder.NewSelectBuilder()
|
||||||
|
|
||||||
|
sb.Select(
|
||||||
|
"timestamp",
|
||||||
|
"trace_id",
|
||||||
|
"span_id",
|
||||||
|
"name",
|
||||||
|
"service.name",
|
||||||
|
"duration_nano",
|
||||||
|
"parent_span_id",
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, field := range b.operator.SelectFields {
|
||||||
|
colExpr, err := b.stmtBuilder.fm.ColumnExpressionFor(b.ctx, &field, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.NewInvalidInputf(
|
||||||
|
errors.CodeInvalidInput,
|
||||||
|
"failed to map select field '%s' in list query: %v",
|
||||||
|
field.Name,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
sb.SelectMore(sqlbuilder.Escape(colExpr))
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.From(selectFromCTE)
|
||||||
|
|
||||||
|
// Add order by support
|
||||||
|
keySelectors := b.getKeySelectors()
|
||||||
|
keys, _, err := b.stmtBuilder.metadataStore.GetKeysMulti(b.ctx, keySelectors)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
orderApplied := false
|
||||||
|
for _, orderBy := range b.operator.Order {
|
||||||
|
colExpr, err := b.stmtBuilder.fm.ColumnExpressionFor(b.ctx, &orderBy.Key.TelemetryFieldKey, keys)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
sb.OrderBy(fmt.Sprintf("%s %s", colExpr, orderBy.Direction.StringValue()))
|
||||||
|
orderApplied = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if !orderApplied {
|
||||||
|
sb.OrderBy("timestamp DESC")
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.operator.Limit > 0 {
|
||||||
|
sb.Limit(b.operator.Limit)
|
||||||
|
} else {
|
||||||
|
sb.Limit(100)
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.operator.Offset > 0 {
|
||||||
|
sb.Offset(b.operator.Offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
sql, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||||
|
return &qbtypes.Statement{
|
||||||
|
Query: sql,
|
||||||
|
Args: args,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *traceOperatorCTEBuilder) getKeySelectors() []*telemetrytypes.FieldKeySelector {
|
||||||
|
var keySelectors []*telemetrytypes.FieldKeySelector
|
||||||
|
|
||||||
|
for _, agg := range b.operator.Aggregations {
|
||||||
|
selectors := querybuilder.QueryStringToKeysSelectors(agg.Expression)
|
||||||
|
keySelectors = append(keySelectors, selectors...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.operator.Filter != nil && b.operator.Filter.Expression != "" {
|
||||||
|
selectors := querybuilder.QueryStringToKeysSelectors(b.operator.Filter.Expression)
|
||||||
|
keySelectors = append(keySelectors, selectors...)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, gb := range b.operator.GroupBy {
|
||||||
|
selectors := querybuilder.QueryStringToKeysSelectors(gb.TelemetryFieldKey.Name)
|
||||||
|
keySelectors = append(keySelectors, selectors...)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, order := range b.operator.Order {
|
||||||
|
keySelectors = append(keySelectors, &telemetrytypes.FieldKeySelector{
|
||||||
|
Name: order.Key.Name,
|
||||||
|
Signal: telemetrytypes.SignalTraces,
|
||||||
|
FieldContext: order.Key.FieldContext,
|
||||||
|
FieldDataType: order.Key.FieldDataType,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range keySelectors {
|
||||||
|
keySelectors[i].Signal = telemetrytypes.SignalTraces
|
||||||
|
}
|
||||||
|
|
||||||
|
return keySelectors
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *traceOperatorCTEBuilder) getRequiredAttributeColumns() []string {
|
||||||
|
requiredColumns := make(map[string]bool)
|
||||||
|
|
||||||
|
allKeySelectors := b.getKeySelectors()
|
||||||
|
|
||||||
|
for _, selector := range allKeySelectors {
|
||||||
|
if b.isIntrinsicField(selector.Name) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.ToLower(selector.Name) == SpanSearchScopeRoot || strings.ToLower(selector.Name) == SpanSearchScopeEntryPoint {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch selector.FieldContext {
|
||||||
|
case telemetrytypes.FieldContextResource:
|
||||||
|
requiredColumns["resources_string"] = true
|
||||||
|
case telemetrytypes.FieldContextAttribute, telemetrytypes.FieldContextSpan, telemetrytypes.FieldContextUnspecified:
|
||||||
|
switch selector.FieldDataType {
|
||||||
|
case telemetrytypes.FieldDataTypeString:
|
||||||
|
requiredColumns["attributes_string"] = true
|
||||||
|
case telemetrytypes.FieldDataTypeNumber:
|
||||||
|
requiredColumns["attributes_number"] = true
|
||||||
|
case telemetrytypes.FieldDataTypeBool:
|
||||||
|
requiredColumns["attributes_bool"] = true
|
||||||
|
default:
|
||||||
|
requiredColumns["attributes_string"] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make([]string, 0, len(requiredColumns))
|
||||||
|
for col := range requiredColumns {
|
||||||
|
result = append(result, col)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *traceOperatorCTEBuilder) isIntrinsicField(fieldName string) bool {
|
||||||
|
_, isIntrinsic := IntrinsicFields[fieldName]
|
||||||
|
if isIntrinsic {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
_, isIntrinsicDeprecated := IntrinsicFieldsDeprecated[fieldName]
|
||||||
|
if isIntrinsicDeprecated {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
_, isCalculated := CalculatedFields[fieldName]
|
||||||
|
if isCalculated {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
_, isCalculatedDeprecated := CalculatedFieldsDeprecated[fieldName]
|
||||||
|
if isCalculatedDeprecated {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
_, isDefault := DefaultFields[fieldName]
|
||||||
|
return isDefault
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *traceOperatorCTEBuilder) buildTimeSeriesQuery(selectFromCTE string) (*qbtypes.Statement, error) {
|
||||||
|
sb := sqlbuilder.NewSelectBuilder()
|
||||||
|
|
||||||
|
stepIntervalSeconds := int64(b.operator.StepInterval.Seconds())
|
||||||
|
if stepIntervalSeconds <= 0 {
|
||||||
|
timeRangeSeconds := (b.end - b.start) / querybuilder.NsToSeconds
|
||||||
|
if timeRangeSeconds > 3600 {
|
||||||
|
stepIntervalSeconds = 300
|
||||||
|
} else if timeRangeSeconds > 1800 {
|
||||||
|
stepIntervalSeconds = 120
|
||||||
|
} else {
|
||||||
|
stepIntervalSeconds = 60
|
||||||
|
}
|
||||||
|
|
||||||
|
b.stmtBuilder.logger.WarnContext(b.ctx,
|
||||||
|
"trace operator stepInterval not set, using default",
|
||||||
|
"defaultSeconds", stepIntervalSeconds)
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.Select(fmt.Sprintf(
|
||||||
|
"toStartOfInterval(timestamp, INTERVAL %d SECOND) AS ts",
|
||||||
|
stepIntervalSeconds,
|
||||||
|
))
|
||||||
|
|
||||||
|
keySelectors := b.getKeySelectors()
|
||||||
|
keys, _, err := b.stmtBuilder.metadataStore.GetKeysMulti(b.ctx, keySelectors)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var allGroupByArgs []any
|
||||||
|
|
||||||
|
for _, gb := range b.operator.GroupBy {
|
||||||
|
expr, args, err := querybuilder.CollisionHandledFinalExpr(
|
||||||
|
b.ctx,
|
||||||
|
&gb.TelemetryFieldKey,
|
||||||
|
b.stmtBuilder.fm,
|
||||||
|
b.stmtBuilder.cb,
|
||||||
|
keys,
|
||||||
|
telemetrytypes.FieldDataTypeString,
|
||||||
|
"",
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.NewInvalidInputf(
|
||||||
|
errors.CodeInvalidInput,
|
||||||
|
"failed to map group by field '%s': %v",
|
||||||
|
gb.TelemetryFieldKey.Name,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
colExpr := fmt.Sprintf("toString(%s) AS `%s`", expr, gb.TelemetryFieldKey.Name)
|
||||||
|
allGroupByArgs = append(allGroupByArgs, args...)
|
||||||
|
sb.SelectMore(colExpr)
|
||||||
|
}
|
||||||
|
|
||||||
|
var allAggChArgs []any
|
||||||
|
for i, agg := range b.operator.Aggregations {
|
||||||
|
rewritten, chArgs, err := b.stmtBuilder.aggExprRewriter.Rewrite(
|
||||||
|
b.ctx,
|
||||||
|
agg.Expression,
|
||||||
|
uint64(stepIntervalSeconds),
|
||||||
|
keys,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.NewInvalidInputf(
|
||||||
|
errors.CodeInvalidInput,
|
||||||
|
"failed to rewrite aggregation expression '%s': %v",
|
||||||
|
agg.Expression,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
allAggChArgs = append(allAggChArgs, chArgs...)
|
||||||
|
|
||||||
|
alias := fmt.Sprintf("__result_%d", i)
|
||||||
|
|
||||||
|
sb.SelectMore(fmt.Sprintf("%s AS %s", rewritten, alias))
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.From(selectFromCTE)
|
||||||
|
|
||||||
|
sb.GroupBy("ts")
|
||||||
|
if len(b.operator.GroupBy) > 0 {
|
||||||
|
groupByKeys := make([]string, len(b.operator.GroupBy))
|
||||||
|
for i, gb := range b.operator.GroupBy {
|
||||||
|
groupByKeys[i] = fmt.Sprintf("`%s`", gb.TelemetryFieldKey.Name)
|
||||||
|
}
|
||||||
|
sb.GroupBy(groupByKeys...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add order by support
|
||||||
|
for _, orderBy := range b.operator.Order {
|
||||||
|
idx, ok := b.aggOrderBy(orderBy)
|
||||||
|
if ok {
|
||||||
|
sb.OrderBy(fmt.Sprintf("__result_%d %s", idx, orderBy.Direction.StringValue()))
|
||||||
|
} else {
|
||||||
|
sb.OrderBy(fmt.Sprintf("`%s` %s", orderBy.Key.Name, orderBy.Direction.StringValue()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sb.OrderBy("ts desc")
|
||||||
|
|
||||||
|
combinedArgs := append(allGroupByArgs, allAggChArgs...)
|
||||||
|
|
||||||
|
// Add HAVING clause if specified
|
||||||
|
if err := b.addHavingClause(sb); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add limit support
|
||||||
|
if b.operator.Limit > 0 {
|
||||||
|
sb.Limit(b.operator.Limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
sql, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse, combinedArgs...)
|
||||||
|
return &qbtypes.Statement{
|
||||||
|
Query: sql,
|
||||||
|
Args: args,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *traceOperatorCTEBuilder) buildTraceQuery(selectFromCTE string) (*qbtypes.Statement, error) {
|
||||||
|
sb := sqlbuilder.NewSelectBuilder()
|
||||||
|
|
||||||
|
keySelectors := b.getKeySelectors()
|
||||||
|
keys, _, err := b.stmtBuilder.metadataStore.GetKeysMulti(b.ctx, keySelectors)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var allGroupByArgs []any
|
||||||
|
|
||||||
|
for _, gb := range b.operator.GroupBy {
|
||||||
|
expr, args, err := querybuilder.CollisionHandledFinalExpr(
|
||||||
|
b.ctx,
|
||||||
|
&gb.TelemetryFieldKey,
|
||||||
|
b.stmtBuilder.fm,
|
||||||
|
b.stmtBuilder.cb,
|
||||||
|
keys,
|
||||||
|
telemetrytypes.FieldDataTypeString,
|
||||||
|
"",
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.NewInvalidInputf(
|
||||||
|
errors.CodeInvalidInput,
|
||||||
|
"failed to map group by field '%s': %v",
|
||||||
|
gb.TelemetryFieldKey.Name,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
colExpr := fmt.Sprintf("toString(%s) AS `%s`", expr, gb.TelemetryFieldKey.Name)
|
||||||
|
allGroupByArgs = append(allGroupByArgs, args...)
|
||||||
|
sb.SelectMore(colExpr)
|
||||||
|
}
|
||||||
|
|
||||||
|
rateInterval := (b.end - b.start) / querybuilder.NsToSeconds
|
||||||
|
|
||||||
|
var allAggChArgs []any
|
||||||
|
for i, agg := range b.operator.Aggregations {
|
||||||
|
rewritten, chArgs, err := b.stmtBuilder.aggExprRewriter.Rewrite(
|
||||||
|
b.ctx,
|
||||||
|
agg.Expression,
|
||||||
|
rateInterval,
|
||||||
|
keys,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.NewInvalidInputf(
|
||||||
|
errors.CodeInvalidInput,
|
||||||
|
"failed to rewrite aggregation expression '%s': %v",
|
||||||
|
agg.Expression,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
allAggChArgs = append(allAggChArgs, chArgs...)
|
||||||
|
|
||||||
|
alias := fmt.Sprintf("__result_%d", i)
|
||||||
|
|
||||||
|
sb.SelectMore(fmt.Sprintf("%s AS %s", rewritten, alias))
|
||||||
|
}
|
||||||
|
|
||||||
|
traceSubquery := fmt.Sprintf("SELECT DISTINCT trace_id FROM %s", selectFromCTE)
|
||||||
|
|
||||||
|
sb.Select(
|
||||||
|
"any(timestamp) as timestamp",
|
||||||
|
"any(`service.name`) as `service.name`",
|
||||||
|
"any(name) as `name`",
|
||||||
|
"count() as span_count",
|
||||||
|
"any(duration_nano) as `duration_nano`",
|
||||||
|
"trace_id as `trace_id`",
|
||||||
|
)
|
||||||
|
|
||||||
|
sb.From("base_spans")
|
||||||
|
sb.Where(
|
||||||
|
fmt.Sprintf("trace_id GLOBAL IN (%s)", traceSubquery),
|
||||||
|
"parent_span_id = ''",
|
||||||
|
)
|
||||||
|
|
||||||
|
sb.GroupBy("trace_id")
|
||||||
|
if len(b.operator.GroupBy) > 0 {
|
||||||
|
groupByKeys := make([]string, len(b.operator.GroupBy))
|
||||||
|
for i, gb := range b.operator.GroupBy {
|
||||||
|
groupByKeys[i] = fmt.Sprintf("`%s`", gb.TelemetryFieldKey.Name)
|
||||||
|
}
|
||||||
|
sb.GroupBy(groupByKeys...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add HAVING clause if specified
|
||||||
|
if err := b.addHavingClause(sb); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
orderApplied := false
|
||||||
|
for _, orderBy := range b.operator.Order {
|
||||||
|
switch orderBy.Key.Name {
|
||||||
|
case qbtypes.OrderByTraceDuration.StringValue():
|
||||||
|
sb.OrderBy(fmt.Sprintf("`duration_nano` %s", orderBy.Direction.StringValue()))
|
||||||
|
orderApplied = true
|
||||||
|
case qbtypes.OrderBySpanCount.StringValue():
|
||||||
|
sb.OrderBy(fmt.Sprintf("span_count %s", orderBy.Direction.StringValue()))
|
||||||
|
orderApplied = true
|
||||||
|
case "timestamp":
|
||||||
|
sb.OrderBy(fmt.Sprintf("timestamp %s", orderBy.Direction.StringValue()))
|
||||||
|
orderApplied = true
|
||||||
|
default:
|
||||||
|
aggIndex := -1
|
||||||
|
for i, agg := range b.operator.Aggregations {
|
||||||
|
if orderBy.Key.Name == agg.Alias || orderBy.Key.Name == fmt.Sprintf("__result_%d", i) {
|
||||||
|
aggIndex = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if aggIndex >= 0 {
|
||||||
|
alias := fmt.Sprintf("__result_%d", aggIndex)
|
||||||
|
if b.operator.Aggregations[aggIndex].Alias != "" {
|
||||||
|
alias = b.operator.Aggregations[aggIndex].Alias
|
||||||
|
}
|
||||||
|
sb.OrderBy(fmt.Sprintf("%s %s", alias, orderBy.Direction.StringValue()))
|
||||||
|
orderApplied = true
|
||||||
|
} else {
|
||||||
|
b.stmtBuilder.logger.WarnContext(b.ctx,
|
||||||
|
"ignoring order by field that's not available in trace context",
|
||||||
|
"field", orderBy.Key.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !orderApplied {
|
||||||
|
sb.OrderBy("`duration_nano` DESC")
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.operator.Limit > 0 {
|
||||||
|
sb.Limit(b.operator.Limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
combinedArgs := append(allGroupByArgs, allAggChArgs...)
|
||||||
|
|
||||||
|
sql, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse, combinedArgs...)
|
||||||
|
return &qbtypes.Statement{
|
||||||
|
Query: sql,
|
||||||
|
Args: args,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *traceOperatorCTEBuilder) buildScalarQuery(selectFromCTE string) (*qbtypes.Statement, error) {
|
||||||
|
sb := sqlbuilder.NewSelectBuilder()
|
||||||
|
|
||||||
|
keySelectors := b.getKeySelectors()
|
||||||
|
keys, _, err := b.stmtBuilder.metadataStore.GetKeysMulti(b.ctx, keySelectors)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var allGroupByArgs []any
|
||||||
|
|
||||||
|
for _, gb := range b.operator.GroupBy {
|
||||||
|
expr, args, err := querybuilder.CollisionHandledFinalExpr(
|
||||||
|
b.ctx,
|
||||||
|
&gb.TelemetryFieldKey,
|
||||||
|
b.stmtBuilder.fm,
|
||||||
|
b.stmtBuilder.cb,
|
||||||
|
keys,
|
||||||
|
telemetrytypes.FieldDataTypeString,
|
||||||
|
"",
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.NewInvalidInputf(
|
||||||
|
errors.CodeInvalidInput,
|
||||||
|
"failed to map group by field '%s': %v",
|
||||||
|
gb.TelemetryFieldKey.Name,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
colExpr := fmt.Sprintf("toString(%s) AS `%s`", expr, gb.TelemetryFieldKey.Name)
|
||||||
|
allGroupByArgs = append(allGroupByArgs, args...)
|
||||||
|
sb.SelectMore(colExpr)
|
||||||
|
}
|
||||||
|
|
||||||
|
var allAggChArgs []any
|
||||||
|
for i, agg := range b.operator.Aggregations {
|
||||||
|
rewritten, chArgs, err := b.stmtBuilder.aggExprRewriter.Rewrite(
|
||||||
|
b.ctx,
|
||||||
|
agg.Expression,
|
||||||
|
uint64((b.end-b.start)/querybuilder.NsToSeconds),
|
||||||
|
keys,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.NewInvalidInputf(
|
||||||
|
errors.CodeInvalidInput,
|
||||||
|
"failed to rewrite aggregation expression '%s': %v",
|
||||||
|
agg.Expression,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
allAggChArgs = append(allAggChArgs, chArgs...)
|
||||||
|
|
||||||
|
alias := fmt.Sprintf("__result_%d", i)
|
||||||
|
|
||||||
|
sb.SelectMore(fmt.Sprintf("%s AS %s", rewritten, alias))
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.From(selectFromCTE)
|
||||||
|
|
||||||
|
if len(b.operator.GroupBy) > 0 {
|
||||||
|
groupByKeys := make([]string, len(b.operator.GroupBy))
|
||||||
|
for i, gb := range b.operator.GroupBy {
|
||||||
|
groupByKeys[i] = fmt.Sprintf("`%s`", gb.TelemetryFieldKey.Name)
|
||||||
|
}
|
||||||
|
sb.GroupBy(groupByKeys...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add order by support
|
||||||
|
for _, orderBy := range b.operator.Order {
|
||||||
|
idx, ok := b.aggOrderBy(orderBy)
|
||||||
|
if ok {
|
||||||
|
sb.OrderBy(fmt.Sprintf("__result_%d %s", idx, orderBy.Direction.StringValue()))
|
||||||
|
} else {
|
||||||
|
sb.OrderBy(fmt.Sprintf("`%s` %s", orderBy.Key.Name, orderBy.Direction.StringValue()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add default ordering if no orderBy specified
|
||||||
|
if len(b.operator.Order) == 0 {
|
||||||
|
sb.OrderBy("__result_0 DESC")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add limit support
|
||||||
|
if b.operator.Limit > 0 {
|
||||||
|
sb.Limit(b.operator.Limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
combinedArgs := append(allGroupByArgs, allAggChArgs...)
|
||||||
|
|
||||||
|
// Add HAVING clause if specified
|
||||||
|
if err := b.addHavingClause(sb); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
sql, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse, combinedArgs...)
|
||||||
|
return &qbtypes.Statement{
|
||||||
|
Query: sql,
|
||||||
|
Args: args,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *traceOperatorCTEBuilder) addHavingClause(sb *sqlbuilder.SelectBuilder) error {
|
||||||
|
if b.operator.Having != nil && b.operator.Having.Expression != "" {
|
||||||
|
rewriter := querybuilder.NewHavingExpressionRewriter()
|
||||||
|
rewrittenExpr := rewriter.RewriteForTraces(b.operator.Having.Expression, b.operator.Aggregations)
|
||||||
|
sb.Having(rewrittenExpr)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *traceOperatorCTEBuilder) addCTE(name, sql string, args []any, dependsOn []string) {
|
||||||
|
b.ctes = append(b.ctes, cteNode{
|
||||||
|
name: name,
|
||||||
|
sql: sql,
|
||||||
|
args: args,
|
||||||
|
dependsOn: dependsOn,
|
||||||
|
})
|
||||||
|
b.cteNameToIndex[name] = len(b.ctes) - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *traceOperatorCTEBuilder) aggOrderBy(k qbtypes.OrderBy) (int, bool) {
|
||||||
|
for i, agg := range b.operator.Aggregations {
|
||||||
|
if k.Key.Name == agg.Alias ||
|
||||||
|
k.Key.Name == agg.Expression ||
|
||||||
|
k.Key.Name == fmt.Sprintf("__result_%d", i) {
|
||||||
|
return i, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
89
pkg/telemetrytraces/trace_operator_statement_builder.go
Normal file
89
pkg/telemetrytraces/trace_operator_statement_builder.go
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
package telemetrytraces
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/SigNoz/signoz/pkg/errors"
|
||||||
|
"github.com/SigNoz/signoz/pkg/factory"
|
||||||
|
"github.com/SigNoz/signoz/pkg/querybuilder"
|
||||||
|
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||||
|
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||||
|
"log/slog"
|
||||||
|
)
|
||||||
|
|
||||||
|
type traceOperatorStatementBuilder struct {
|
||||||
|
logger *slog.Logger
|
||||||
|
metadataStore telemetrytypes.MetadataStore
|
||||||
|
fm qbtypes.FieldMapper
|
||||||
|
cb qbtypes.ConditionBuilder
|
||||||
|
traceStmtBuilder qbtypes.StatementBuilder[qbtypes.TraceAggregation]
|
||||||
|
aggExprRewriter qbtypes.AggExprRewriter
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ qbtypes.TraceOperatorStatementBuilder = (*traceOperatorStatementBuilder)(nil)
|
||||||
|
|
||||||
|
func NewTraceOperatorStatementBuilder(
|
||||||
|
settings factory.ProviderSettings,
|
||||||
|
metadataStore telemetrytypes.MetadataStore,
|
||||||
|
fieldMapper qbtypes.FieldMapper,
|
||||||
|
conditionBuilder qbtypes.ConditionBuilder,
|
||||||
|
traceStmtBuilder qbtypes.StatementBuilder[qbtypes.TraceAggregation],
|
||||||
|
aggExprRewriter qbtypes.AggExprRewriter,
|
||||||
|
) *traceOperatorStatementBuilder {
|
||||||
|
tracesSettings := factory.NewScopedProviderSettings(settings, "github.com/SigNoz/signoz/pkg/telemetrytraces")
|
||||||
|
return &traceOperatorStatementBuilder{
|
||||||
|
logger: tracesSettings.Logger(),
|
||||||
|
metadataStore: metadataStore,
|
||||||
|
fm: fieldMapper,
|
||||||
|
cb: conditionBuilder,
|
||||||
|
traceStmtBuilder: traceStmtBuilder,
|
||||||
|
aggExprRewriter: aggExprRewriter,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build builds a SQL query based on the given parameters.
|
||||||
|
func (b *traceOperatorStatementBuilder) Build(
|
||||||
|
ctx context.Context,
|
||||||
|
start uint64,
|
||||||
|
end uint64,
|
||||||
|
requestType qbtypes.RequestType,
|
||||||
|
query qbtypes.QueryBuilderTraceOperator,
|
||||||
|
compositeQuery *qbtypes.CompositeQuery,
|
||||||
|
) (*qbtypes.Statement, error) {
|
||||||
|
|
||||||
|
start = querybuilder.ToNanoSecs(start)
|
||||||
|
end = querybuilder.ToNanoSecs(end)
|
||||||
|
|
||||||
|
// Parse the expression if not already parsed
|
||||||
|
if query.ParsedExpression == nil {
|
||||||
|
if err := query.ParseExpression(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate compositeQuery parameter
|
||||||
|
if compositeQuery == nil {
|
||||||
|
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "compositeQuery cannot be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the CTE-based query
|
||||||
|
builder := &traceOperatorCTEBuilder{
|
||||||
|
ctx: ctx,
|
||||||
|
start: start,
|
||||||
|
end: end,
|
||||||
|
operator: &query,
|
||||||
|
stmtBuilder: b,
|
||||||
|
queries: make(map[string]*qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]),
|
||||||
|
ctes: []cteNode{}, // Use slice to maintain order
|
||||||
|
cteNameToIndex: make(map[string]int),
|
||||||
|
queryToCTEName: make(map[string]string),
|
||||||
|
compositeQuery: compositeQuery, // Now passed as explicit parameter
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect all referenced queries
|
||||||
|
if err := builder.collectQueries(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the query
|
||||||
|
return builder.build(requestType)
|
||||||
|
}
|
||||||
@@ -52,3 +52,8 @@ type StatementBuilder[T any] interface {
|
|||||||
// Build builds the query.
|
// Build builds the query.
|
||||||
Build(ctx context.Context, start, end uint64, requestType RequestType, query QueryBuilderQuery[T], variables map[string]VariableItem) (*Statement, error)
|
Build(ctx context.Context, start, end uint64, requestType RequestType, query QueryBuilderQuery[T], variables map[string]VariableItem) (*Statement, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TraceOperatorStatementBuilder interface {
|
||||||
|
// Build builds the trace operator query.
|
||||||
|
Build(ctx context.Context, start, end uint64, requestType RequestType, query QueryBuilderTraceOperator, compositeQuery *CompositeQuery) (*Statement, error)
|
||||||
|
}
|
||||||
|
|||||||
@@ -88,8 +88,8 @@ func (q *QueryEnvelope) UnmarshalJSON(data []byte) error {
|
|||||||
|
|
||||||
case QueryTypeTraceOperator:
|
case QueryTypeTraceOperator:
|
||||||
var spec QueryBuilderTraceOperator
|
var spec QueryBuilderTraceOperator
|
||||||
if err := json.Unmarshal(shadow.Spec, &spec); err != nil {
|
if err := UnmarshalJSONWithContext(shadow.Spec, &spec, "trace operator spec"); err != nil {
|
||||||
return errors.WrapInvalidInputf(err, errors.CodeInvalidInput, "invalid trace operator spec")
|
return wrapUnmarshalError(err, "invalid trace operator spec: %v", err)
|
||||||
}
|
}
|
||||||
q.Spec = spec
|
q.Spec = spec
|
||||||
|
|
||||||
@@ -113,7 +113,7 @@ func (q *QueryEnvelope) UnmarshalJSON(data []byte) error {
|
|||||||
"unknown query type %q",
|
"unknown query type %q",
|
||||||
shadow.Type,
|
shadow.Type,
|
||||||
).WithAdditional(
|
).WithAdditional(
|
||||||
"Valid query types are: builder_query, builder_sub_query, builder_formula, builder_join, promql, clickhouse_sql",
|
"Valid query types are: builder_query, builder_sub_query, builder_formula, builder_join, builder_trace_operator, promql, clickhouse_sql",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ func TestQueryRangeRequest_UnmarshalJSON(t *testing.T) {
|
|||||||
"filter": {
|
"filter": {
|
||||||
"expression": "trace_duration > 200ms AND span_count >= 5"
|
"expression": "trace_duration > 200ms AND span_count >= 5"
|
||||||
},
|
},
|
||||||
"orderBy": [{
|
"order": [{
|
||||||
"key": {
|
"key": {
|
||||||
"name": "trace_duration"
|
"name": "trace_duration"
|
||||||
},
|
},
|
||||||
@@ -230,7 +230,7 @@ func TestQueryRangeRequest_UnmarshalJSON(t *testing.T) {
|
|||||||
"name": "complex_trace_analysis",
|
"name": "complex_trace_analysis",
|
||||||
"expression": "A => (B && NOT C)",
|
"expression": "A => (B && NOT C)",
|
||||||
"filter": { "expression": "trace_duration BETWEEN 100ms AND 5s AND span_count IN (5, 10, 15)" },
|
"filter": { "expression": "trace_duration BETWEEN 100ms AND 5s AND span_count IN (5, 10, 15)" },
|
||||||
"orderBy": [{
|
"order": [{
|
||||||
"key": { "name": "span_count" },
|
"key": { "name": "span_count" },
|
||||||
"direction": "asc"
|
"direction": "asc"
|
||||||
}],
|
}],
|
||||||
@@ -1028,15 +1028,17 @@ func TestQueryRangeRequest_UnmarshalJSON(t *testing.T) {
|
|||||||
|
|
||||||
func TestParseTraceExpression(t *testing.T) {
|
func TestParseTraceExpression(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
expression string
|
expression string
|
||||||
expectError bool
|
expectError bool
|
||||||
checkResult func(t *testing.T, result *TraceOperand)
|
expectedOpCount int
|
||||||
|
checkResult func(t *testing.T, result *TraceOperand)
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "simple query reference",
|
name: "simple query reference",
|
||||||
expression: "A",
|
expression: "A",
|
||||||
expectError: false,
|
expectError: false,
|
||||||
|
expectedOpCount: 0,
|
||||||
checkResult: func(t *testing.T, result *TraceOperand) {
|
checkResult: func(t *testing.T, result *TraceOperand) {
|
||||||
assert.NotNil(t, result.QueryRef)
|
assert.NotNil(t, result.QueryRef)
|
||||||
assert.Equal(t, "A", result.QueryRef.Name)
|
assert.Equal(t, "A", result.QueryRef.Name)
|
||||||
@@ -1044,9 +1046,10 @@ func TestParseTraceExpression(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "simple implication",
|
name: "simple implication",
|
||||||
expression: "A => B",
|
expression: "A => B",
|
||||||
expectError: false,
|
expectError: false,
|
||||||
|
expectedOpCount: 1,
|
||||||
checkResult: func(t *testing.T, result *TraceOperand) {
|
checkResult: func(t *testing.T, result *TraceOperand) {
|
||||||
assert.NotNil(t, result.Operator)
|
assert.NotNil(t, result.Operator)
|
||||||
assert.Equal(t, TraceOperatorDirectDescendant, *result.Operator)
|
assert.Equal(t, TraceOperatorDirectDescendant, *result.Operator)
|
||||||
@@ -1057,9 +1060,10 @@ func TestParseTraceExpression(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "and operation",
|
name: "and operation",
|
||||||
expression: "A && B",
|
expression: "A && B",
|
||||||
expectError: false,
|
expectError: false,
|
||||||
|
expectedOpCount: 1,
|
||||||
checkResult: func(t *testing.T, result *TraceOperand) {
|
checkResult: func(t *testing.T, result *TraceOperand) {
|
||||||
assert.NotNil(t, result.Operator)
|
assert.NotNil(t, result.Operator)
|
||||||
assert.Equal(t, TraceOperatorAnd, *result.Operator)
|
assert.Equal(t, TraceOperatorAnd, *result.Operator)
|
||||||
@@ -1068,9 +1072,10 @@ func TestParseTraceExpression(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "or operation",
|
name: "or operation",
|
||||||
expression: "A || B",
|
expression: "A || B",
|
||||||
expectError: false,
|
expectError: false,
|
||||||
|
expectedOpCount: 1,
|
||||||
checkResult: func(t *testing.T, result *TraceOperand) {
|
checkResult: func(t *testing.T, result *TraceOperand) {
|
||||||
assert.NotNil(t, result.Operator)
|
assert.NotNil(t, result.Operator)
|
||||||
assert.Equal(t, TraceOperatorOr, *result.Operator)
|
assert.Equal(t, TraceOperatorOr, *result.Operator)
|
||||||
@@ -1079,9 +1084,10 @@ func TestParseTraceExpression(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "unary NOT operation",
|
name: "unary NOT operation",
|
||||||
expression: "NOT A",
|
expression: "NOT A",
|
||||||
expectError: false,
|
expectError: false,
|
||||||
|
expectedOpCount: 1,
|
||||||
checkResult: func(t *testing.T, result *TraceOperand) {
|
checkResult: func(t *testing.T, result *TraceOperand) {
|
||||||
assert.NotNil(t, result.Operator)
|
assert.NotNil(t, result.Operator)
|
||||||
assert.Equal(t, TraceOperatorNot, *result.Operator)
|
assert.Equal(t, TraceOperatorNot, *result.Operator)
|
||||||
@@ -1091,9 +1097,10 @@ func TestParseTraceExpression(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "binary NOT operation",
|
name: "binary NOT operation",
|
||||||
expression: "A NOT B",
|
expression: "A NOT B",
|
||||||
expectError: false,
|
expectError: false,
|
||||||
|
expectedOpCount: 1,
|
||||||
checkResult: func(t *testing.T, result *TraceOperand) {
|
checkResult: func(t *testing.T, result *TraceOperand) {
|
||||||
assert.NotNil(t, result.Operator)
|
assert.NotNil(t, result.Operator)
|
||||||
assert.Equal(t, TraceOperatorExclude, *result.Operator)
|
assert.Equal(t, TraceOperatorExclude, *result.Operator)
|
||||||
@@ -1104,9 +1111,10 @@ func TestParseTraceExpression(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "complex expression with precedence",
|
name: "complex expression with precedence",
|
||||||
expression: "A => B && C || D",
|
expression: "A => B && C || D",
|
||||||
expectError: false,
|
expectError: false,
|
||||||
|
expectedOpCount: 3, // Three operators: =>, &&, ||
|
||||||
checkResult: func(t *testing.T, result *TraceOperand) {
|
checkResult: func(t *testing.T, result *TraceOperand) {
|
||||||
// Should parse as: A => (B && (C || D)) due to precedence: NOT > || > && > =>
|
// Should parse as: A => (B && (C || D)) due to precedence: NOT > || > && > =>
|
||||||
// The parsing finds operators from lowest precedence first
|
// The parsing finds operators from lowest precedence first
|
||||||
@@ -1120,9 +1128,10 @@ func TestParseTraceExpression(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "simple parentheses",
|
name: "simple parentheses",
|
||||||
expression: "(A)",
|
expression: "(A)",
|
||||||
expectError: false,
|
expectError: false,
|
||||||
|
expectedOpCount: 0,
|
||||||
checkResult: func(t *testing.T, result *TraceOperand) {
|
checkResult: func(t *testing.T, result *TraceOperand) {
|
||||||
assert.NotNil(t, result.QueryRef)
|
assert.NotNil(t, result.QueryRef)
|
||||||
assert.Equal(t, "A", result.QueryRef.Name)
|
assert.Equal(t, "A", result.QueryRef.Name)
|
||||||
@@ -1130,9 +1139,10 @@ func TestParseTraceExpression(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "parentheses expression",
|
name: "parentheses expression",
|
||||||
expression: "A => (B || C)",
|
expression: "A => (B || C)",
|
||||||
expectError: false,
|
expectError: false,
|
||||||
|
expectedOpCount: 2, // Two operators: =>, ||
|
||||||
checkResult: func(t *testing.T, result *TraceOperand) {
|
checkResult: func(t *testing.T, result *TraceOperand) {
|
||||||
assert.NotNil(t, result.Operator)
|
assert.NotNil(t, result.Operator)
|
||||||
assert.Equal(t, TraceOperatorDirectDescendant, *result.Operator)
|
assert.Equal(t, TraceOperatorDirectDescendant, *result.Operator)
|
||||||
@@ -1146,9 +1156,10 @@ func TestParseTraceExpression(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "nested NOT with parentheses",
|
name: "nested NOT with parentheses",
|
||||||
expression: "NOT (A && B)",
|
expression: "NOT (A && B)",
|
||||||
expectError: false,
|
expectError: false,
|
||||||
|
expectedOpCount: 2, // Two operators: NOT, &&
|
||||||
checkResult: func(t *testing.T, result *TraceOperand) {
|
checkResult: func(t *testing.T, result *TraceOperand) {
|
||||||
assert.NotNil(t, result.Operator)
|
assert.NotNil(t, result.Operator)
|
||||||
assert.Equal(t, TraceOperatorNot, *result.Operator)
|
assert.Equal(t, TraceOperatorNot, *result.Operator)
|
||||||
@@ -1159,6 +1170,13 @@ func TestParseTraceExpression(t *testing.T) {
|
|||||||
assert.Equal(t, TraceOperatorAnd, *result.Left.Operator)
|
assert.Equal(t, TraceOperatorAnd, *result.Left.Operator)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "complex expression exceeding operator limit",
|
||||||
|
expression: "A => B => C => D => E => F => G => H => I => J => K => L",
|
||||||
|
expectError: false, // parseTraceExpression doesn't validate count, ParseExpression does
|
||||||
|
expectedOpCount: 11, // 11 => operators
|
||||||
|
checkResult: nil,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "invalid query reference with numbers",
|
name: "invalid query reference with numbers",
|
||||||
expression: "123",
|
expression: "123",
|
||||||
@@ -1174,11 +1192,11 @@ func TestParseTraceExpression(t *testing.T) {
|
|||||||
expression: "",
|
expression: "",
|
||||||
expectError: true,
|
expectError: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
name: "expression with extra whitespace",
|
name: "expression with extra whitespace",
|
||||||
expression: " A => B ",
|
expression: " A => B ",
|
||||||
expectError: false,
|
expectError: false,
|
||||||
|
expectedOpCount: 1,
|
||||||
checkResult: func(t *testing.T, result *TraceOperand) {
|
checkResult: func(t *testing.T, result *TraceOperand) {
|
||||||
assert.NotNil(t, result.Operator)
|
assert.NotNil(t, result.Operator)
|
||||||
assert.Equal(t, TraceOperatorDirectDescendant, *result.Operator)
|
assert.Equal(t, TraceOperatorDirectDescendant, *result.Operator)
|
||||||
@@ -1190,7 +1208,7 @@ func TestParseTraceExpression(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
result, err := parseTraceExpression(tt.expression)
|
result, opCount, err := parseTraceExpression(tt.expression)
|
||||||
|
|
||||||
if tt.expectError {
|
if tt.expectError {
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
@@ -1200,6 +1218,8 @@ func TestParseTraceExpression(t *testing.T) {
|
|||||||
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, result)
|
require.NotNil(t, result)
|
||||||
|
assert.Equal(t, tt.expectedOpCount, opCount, "operator count mismatch")
|
||||||
|
|
||||||
if tt.checkResult != nil {
|
if tt.checkResult != nil {
|
||||||
tt.checkResult(t, result)
|
tt.checkResult(t, result)
|
||||||
}
|
}
|
||||||
@@ -1207,6 +1227,63 @@ func TestParseTraceExpression(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestQueryBuilderTraceOperator_ParseExpression_OperatorLimit(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
expression string
|
||||||
|
expectError bool
|
||||||
|
errorContains string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "within operator limit",
|
||||||
|
expression: "A => B => C",
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "exceeding operator limit",
|
||||||
|
expression: "A => B => C => D => E => F => G => H => I => J => K => L",
|
||||||
|
expectError: true,
|
||||||
|
errorContains: "expression contains 11 operators, which exceeds the maximum allowed 10 operators",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "exactly at limit",
|
||||||
|
expression: "A => B => C => D => E => F => G => H => I => J => K",
|
||||||
|
expectError: false, // 10 operators, exactly at limit
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "complex expression at limit",
|
||||||
|
expression: "(A && B) => (C || D) => (E && F) => (G || H) => (I && J) => K",
|
||||||
|
expectError: false, // 10 operators: 3 &&, 2 ||, 5 => = 10 total
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "complex expression exceeding limit",
|
||||||
|
expression: "(A && B) => (C || D) => (E && F) => (G || H) => (I && J) => (K || L)",
|
||||||
|
expectError: true,
|
||||||
|
errorContains: "expression contains 11 operators, which exceeds the maximum allowed 10 operators",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
op := &QueryBuilderTraceOperator{
|
||||||
|
Expression: tt.expression,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := op.ParseExpression()
|
||||||
|
|
||||||
|
if tt.expectError {
|
||||||
|
assert.Error(t, err)
|
||||||
|
if tt.errorContains != "" {
|
||||||
|
assert.Contains(t, err.Error(), tt.errorContains)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, op.ParsedExpression)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestQueryBuilderTraceOperator_ValidateTraceOperator(t *testing.T) {
|
func TestQueryBuilderTraceOperator_ValidateTraceOperator(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
|||||||
@@ -29,6 +29,11 @@ var (
|
|||||||
OrderByTraceDuration = TraceOrderBy{valuer.NewString("trace_duration")}
|
OrderByTraceDuration = TraceOrderBy{valuer.NewString("trace_duration")}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// MaxTraceOperators defines the maximum number of operators allowed in a trace expression
|
||||||
|
MaxTraceOperators = 10
|
||||||
|
)
|
||||||
|
|
||||||
type QueryBuilderTraceOperator struct {
|
type QueryBuilderTraceOperator struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Disabled bool `json:"disabled,omitempty"`
|
Disabled bool `json:"disabled,omitempty"`
|
||||||
@@ -41,15 +46,21 @@ type QueryBuilderTraceOperator struct {
|
|||||||
ReturnSpansFrom string `json:"returnSpansFrom,omitempty"`
|
ReturnSpansFrom string `json:"returnSpansFrom,omitempty"`
|
||||||
|
|
||||||
// Trace-specific ordering (only span_count and trace_duration allowed)
|
// Trace-specific ordering (only span_count and trace_duration allowed)
|
||||||
Order []OrderBy `json:"orderBy,omitempty"`
|
Order []OrderBy `json:"order,omitempty"`
|
||||||
|
|
||||||
Aggregations []TraceAggregation `json:"aggregations,omitempty"`
|
Aggregations []TraceAggregation `json:"aggregations,omitempty"`
|
||||||
StepInterval Step `json:"stepInterval,omitempty"`
|
StepInterval Step `json:"stepInterval,omitempty"`
|
||||||
GroupBy []GroupByKey `json:"groupBy,omitempty"`
|
GroupBy []GroupByKey `json:"groupBy,omitempty"`
|
||||||
|
|
||||||
|
// having clause to apply to the aggregated query results
|
||||||
|
Having *Having `json:"having,omitempty"`
|
||||||
|
|
||||||
Limit int `json:"limit,omitempty"`
|
Limit int `json:"limit,omitempty"`
|
||||||
|
Offset int `json:"offset,omitempty"`
|
||||||
Cursor string `json:"cursor,omitempty"`
|
Cursor string `json:"cursor,omitempty"`
|
||||||
|
|
||||||
|
Legend string `json:"legend,omitempty"`
|
||||||
|
|
||||||
// Other post-processing options
|
// Other post-processing options
|
||||||
SelectFields []telemetrytypes.TelemetryFieldKey `json:"selectFields,omitempty"`
|
SelectFields []telemetrytypes.TelemetryFieldKey `json:"selectFields,omitempty"`
|
||||||
Functions []Function `json:"functions,omitempty"`
|
Functions []Function `json:"functions,omitempty"`
|
||||||
@@ -84,7 +95,7 @@ func (q *QueryBuilderTraceOperator) ParseExpression() error {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
parsed, err := parseTraceExpression(q.Expression)
|
parsed, operatorCount, err := parseTraceExpression(q.Expression)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WrapInvalidInputf(
|
return errors.WrapInvalidInputf(
|
||||||
err,
|
err,
|
||||||
@@ -94,13 +105,24 @@ func (q *QueryBuilderTraceOperator) ParseExpression() error {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate operator count immediately during parsing
|
||||||
|
if operatorCount > MaxTraceOperators {
|
||||||
|
return errors.WrapInvalidInputf(
|
||||||
|
nil,
|
||||||
|
errors.CodeInvalidInput,
|
||||||
|
"expression contains %d operators, which exceeds the maximum allowed %d operators",
|
||||||
|
operatorCount,
|
||||||
|
MaxTraceOperators,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
q.ParsedExpression = parsed
|
q.ParsedExpression = parsed
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateTraceOperator validates that all referenced queries exist and are trace queries
|
// ValidateTraceOperator validates that all referenced queries exist and are trace queries
|
||||||
func (q *QueryBuilderTraceOperator) ValidateTraceOperator(queries []QueryEnvelope) error {
|
func (q *QueryBuilderTraceOperator) ValidateTraceOperator(queries []QueryEnvelope) error {
|
||||||
// Parse the expression
|
// Parse the expression - this now includes operator count validation
|
||||||
if err := q.ParseExpression(); err != nil {
|
if err := q.ParseExpression(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -157,6 +179,15 @@ func (q *QueryBuilderTraceOperator) ValidateTraceOperator(queries []QueryEnvelop
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if q.StepInterval.Seconds() < 0 {
|
||||||
|
return errors.WrapInvalidInputf(
|
||||||
|
nil,
|
||||||
|
errors.CodeInvalidInput,
|
||||||
|
"stepInterval cannot be negative, got %f seconds",
|
||||||
|
q.StepInterval.Seconds(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Validate ReturnSpansFrom if specified
|
// Validate ReturnSpansFrom if specified
|
||||||
if q.ReturnSpansFrom != "" {
|
if q.ReturnSpansFrom != "" {
|
||||||
if _, exists := availableQueries[q.ReturnSpansFrom]; !exists {
|
if _, exists := availableQueries[q.ReturnSpansFrom]; !exists {
|
||||||
@@ -234,6 +265,15 @@ func (q *QueryBuilderTraceOperator) ValidatePagination() error {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if q.Offset < 0 {
|
||||||
|
return errors.WrapInvalidInputf(
|
||||||
|
nil,
|
||||||
|
errors.CodeInvalidInput,
|
||||||
|
"offset must be non-negative, got %d",
|
||||||
|
q.Offset,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// For production use, you might want to enforce maximum limits
|
// For production use, you might want to enforce maximum limits
|
||||||
if q.Limit > 10000 {
|
if q.Limit > 10000 {
|
||||||
return errors.WrapInvalidInputf(
|
return errors.WrapInvalidInputf(
|
||||||
@@ -276,6 +316,11 @@ func (q *QueryBuilderTraceOperator) collectReferencedQueries(operand *TraceOpera
|
|||||||
return unique
|
return unique
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CollectReferencedQueries is a public wrapper for collectReferencedQueries
|
||||||
|
func (q *QueryBuilderTraceOperator) CollectReferencedQueries(operand *TraceOperand) []string {
|
||||||
|
return q.collectReferencedQueries(operand)
|
||||||
|
}
|
||||||
|
|
||||||
// ValidateUniqueTraceOperator ensures only one trace operator exists in queries
|
// ValidateUniqueTraceOperator ensures only one trace operator exists in queries
|
||||||
func ValidateUniqueTraceOperator(queries []QueryEnvelope) error {
|
func ValidateUniqueTraceOperator(queries []QueryEnvelope) error {
|
||||||
traceOperatorCount := 0
|
traceOperatorCount := 0
|
||||||
@@ -304,9 +349,8 @@ func ValidateUniqueTraceOperator(queries []QueryEnvelope) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseTraceExpression parses an expression string into a tree structure
|
|
||||||
// Handles precedence: NOT (highest) > || > && > => (lowest)
|
// Handles precedence: NOT (highest) > || > && > => (lowest)
|
||||||
func parseTraceExpression(expr string) (*TraceOperand, error) {
|
func parseTraceExpression(expr string) (*TraceOperand, int, error) {
|
||||||
expr = strings.TrimSpace(expr)
|
expr = strings.TrimSpace(expr)
|
||||||
|
|
||||||
// Handle parentheses
|
// Handle parentheses
|
||||||
@@ -319,15 +363,15 @@ func parseTraceExpression(expr string) (*TraceOperand, error) {
|
|||||||
|
|
||||||
// Handle unary NOT operator (prefix)
|
// Handle unary NOT operator (prefix)
|
||||||
if strings.HasPrefix(expr, "NOT ") {
|
if strings.HasPrefix(expr, "NOT ") {
|
||||||
operand, err := parseTraceExpression(expr[4:])
|
operand, count, err := parseTraceExpression(expr[4:])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
notOp := TraceOperatorNot
|
notOp := TraceOperatorNot
|
||||||
return &TraceOperand{
|
return &TraceOperand{
|
||||||
Operator: ¬Op,
|
Operator: ¬Op,
|
||||||
Left: operand,
|
Left: operand,
|
||||||
}, nil
|
}, count + 1, nil // Add 1 for this NOT operator
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find binary operators with lowest precedence first (=> has lowest precedence)
|
// Find binary operators with lowest precedence first (=> has lowest precedence)
|
||||||
@@ -339,14 +383,14 @@ func parseTraceExpression(expr string) (*TraceOperand, error) {
|
|||||||
leftExpr := strings.TrimSpace(expr[:pos])
|
leftExpr := strings.TrimSpace(expr[:pos])
|
||||||
rightExpr := strings.TrimSpace(expr[pos+len(op):])
|
rightExpr := strings.TrimSpace(expr[pos+len(op):])
|
||||||
|
|
||||||
left, err := parseTraceExpression(leftExpr)
|
left, leftCount, err := parseTraceExpression(leftExpr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
right, err := parseTraceExpression(rightExpr)
|
right, rightCount, err := parseTraceExpression(rightExpr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var opType TraceOperatorType
|
var opType TraceOperatorType
|
||||||
@@ -365,13 +409,13 @@ func parseTraceExpression(expr string) (*TraceOperand, error) {
|
|||||||
Operator: &opType,
|
Operator: &opType,
|
||||||
Left: left,
|
Left: left,
|
||||||
Right: right,
|
Right: right,
|
||||||
}, nil
|
}, leftCount + rightCount + 1, nil // Add counts from both sides + 1 for this operator
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no operators found, this should be a query reference
|
// If no operators found, this should be a query reference
|
||||||
if matched, _ := regexp.MatchString(`^[A-Za-z][A-Za-z0-9_]*$`, expr); !matched {
|
if matched, _ := regexp.MatchString(`^[A-Za-z][A-Za-z0-9_]*$`, expr); !matched {
|
||||||
return nil, errors.WrapInvalidInputf(
|
return nil, 0, errors.WrapInvalidInputf(
|
||||||
nil,
|
nil,
|
||||||
errors.CodeInvalidInput,
|
errors.CodeInvalidInput,
|
||||||
"invalid query reference '%s'",
|
"invalid query reference '%s'",
|
||||||
@@ -379,9 +423,10 @@ func parseTraceExpression(expr string) (*TraceOperand, error) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Leaf node - no operators
|
||||||
return &TraceOperand{
|
return &TraceOperand{
|
||||||
QueryRef: &TraceOperatorQueryRef{Name: expr},
|
QueryRef: &TraceOperatorQueryRef{Name: expr},
|
||||||
}, nil
|
}, 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// isBalancedParentheses checks if parentheses are balanced in the expression
|
// isBalancedParentheses checks if parentheses are balanced in the expression
|
||||||
|
|||||||
@@ -36,6 +36,11 @@ func getQueryIdentifier(envelope QueryEnvelope, index int) string {
|
|||||||
return fmt.Sprintf("formula '%s'", spec.Name)
|
return fmt.Sprintf("formula '%s'", spec.Name)
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("formula at position %d", index+1)
|
return fmt.Sprintf("formula at position %d", index+1)
|
||||||
|
case QueryTypeTraceOperator:
|
||||||
|
if spec, ok := envelope.Spec.(QueryBuilderTraceOperator); ok && spec.Name != "" {
|
||||||
|
return fmt.Sprintf("trace operator '%s'", spec.Name)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("trace operator at position %d", index+1)
|
||||||
case QueryTypeJoin:
|
case QueryTypeJoin:
|
||||||
if spec, ok := envelope.Spec.(QueryBuilderJoin); ok && spec.Name != "" {
|
if spec, ok := envelope.Spec.(QueryBuilderJoin); ok && spec.Name != "" {
|
||||||
return fmt.Sprintf("join '%s'", spec.Name)
|
return fmt.Sprintf("join '%s'", spec.Name)
|
||||||
@@ -564,6 +569,24 @@ func (r *QueryRangeRequest) validateCompositeQuery() error {
|
|||||||
queryId,
|
queryId,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
case QueryTypeTraceOperator:
|
||||||
|
spec, ok := envelope.Spec.(QueryBuilderTraceOperator)
|
||||||
|
if !ok {
|
||||||
|
queryId := getQueryIdentifier(envelope, i)
|
||||||
|
return errors.NewInvalidInputf(
|
||||||
|
errors.CodeInvalidInput,
|
||||||
|
"invalid spec for %s",
|
||||||
|
queryId,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if spec.Expression == "" {
|
||||||
|
queryId := getQueryIdentifier(envelope, i)
|
||||||
|
return errors.NewInvalidInputf(
|
||||||
|
errors.CodeInvalidInput,
|
||||||
|
"expression is required for %s",
|
||||||
|
queryId,
|
||||||
|
)
|
||||||
|
}
|
||||||
case QueryTypePromQL:
|
case QueryTypePromQL:
|
||||||
// PromQL validation is handled separately
|
// PromQL validation is handled separately
|
||||||
spec, ok := envelope.Spec.(PromQuery)
|
spec, ok := envelope.Spec.(PromQuery)
|
||||||
@@ -610,7 +633,7 @@ func (r *QueryRangeRequest) validateCompositeQuery() error {
|
|||||||
envelope.Type,
|
envelope.Type,
|
||||||
queryId,
|
queryId,
|
||||||
).WithAdditional(
|
).WithAdditional(
|
||||||
"Valid query types are: builder_query, builder_formula, builder_join, promql, clickhouse_sql",
|
"Valid query types are: builder_query, builder_formula, builder_join, promql, clickhouse_sql, trace_operator",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -678,6 +701,21 @@ func validateQueryEnvelope(envelope QueryEnvelope, requestType RequestType) erro
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
case QueryTypeTraceOperator:
|
||||||
|
spec, ok := envelope.Spec.(QueryBuilderTraceOperator)
|
||||||
|
if !ok {
|
||||||
|
return errors.NewInvalidInputf(
|
||||||
|
errors.CodeInvalidInput,
|
||||||
|
"invalid trace operator spec",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if spec.Expression == "" {
|
||||||
|
return errors.NewInvalidInputf(
|
||||||
|
errors.CodeInvalidInput,
|
||||||
|
"trace operator expression is required",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
case QueryTypePromQL:
|
case QueryTypePromQL:
|
||||||
spec, ok := envelope.Spec.(PromQuery)
|
spec, ok := envelope.Spec.(PromQuery)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -714,7 +752,7 @@ func validateQueryEnvelope(envelope QueryEnvelope, requestType RequestType) erro
|
|||||||
"unknown query type: %s",
|
"unknown query type: %s",
|
||||||
envelope.Type,
|
envelope.Type,
|
||||||
).WithAdditional(
|
).WithAdditional(
|
||||||
"Valid query types are: builder_query, builder_sub_query, builder_formula, builder_join, promql, clickhouse_sql",
|
"Valid query types are: builder_query, builder_sub_query, builder_formula, builder_join, promql, clickhouse_sql, trace_operator",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user