Compare commits

..

6 Commits

Author SHA1 Message Date
Srikanth Chekuri
a7ddd2ddf0 chore: do not send field context as tag for deprecated fields (#8902) 2025-08-24 11:14:12 +05:30
Srikanth Chekuri
4d72f47758 chore: parse into number alias for mat column from statement (#8900) 2025-08-24 09:44:58 +05:30
Vikrant Gupta
b5b513f1e0 chore(meter): add warnings and make meter live in sidenav (#8882)
* chore(meter): add warnings and make meter live in sidenav

* chore(meter): add warnings and make meter live in sidenav

* chore(meter): add warnings and make meter live in sidenav

* chore(meter): add warnings and make meter live in sidenav

* chore(meter): add warnings and make meter live in sidenav

* chore(meter): add warnings and make meter live in sidenav
2025-08-23 15:00:07 +05:30
Nityananda Gohain
4878f725ea fix: use lower and convert re2 to string in fulltext (#8887)
* fix: use lower and convert re2 to string in fulltext

* fix: minor error change

* fix: address comments

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2025-08-22 20:20:42 +05:30
Srikanth Chekuri
eca13075e9 fix: related links for rule history page (#8883) 2025-08-22 16:19:27 +05:30
Amlan Kumar Nandy
e5ab664483 fix: resolve sentry issues in alert list (#8878)
* fix: resolve sentry issues in alert list

* chore: update the key

---------

Co-authored-by: srikanthccv <srikanth.chekuri92@gmail.com>
2025-08-21 19:21:15 +05:30
60 changed files with 493 additions and 2659 deletions

View File

@@ -5,10 +5,7 @@ import getStartEndRangeTime from 'lib/getStartEndRangeTime';
import { mapQueryDataToApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataToApi';
import { isEmpty } from 'lodash-es';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import {
IBuilderQuery,
IBuilderTraceOperator,
} from 'types/api/queryBuilder/queryBuilderData';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import {
BaseBuilderQuery,
FieldContext,
@@ -69,9 +66,46 @@ function getSignalType(dataSource: string): 'traces' | 'logs' | 'metrics' {
return 'metrics';
}
/**
* Creates base spec for builder queries
*/
function isDeprecatedField(fieldName: string): boolean {
const deprecatedIntrinsicFields = [
'traceID',
'spanID',
'parentSpanID',
'spanKind',
'durationNano',
'statusCode',
'statusMessage',
'statusCodeString',
];
const deprecatedCalculatedFields = [
'responseStatusCode',
'externalHttpUrl',
'httpUrl',
'externalHttpMethod',
'httpMethod',
'httpHost',
'dbName',
'dbOperation',
'hasError',
'isRemote',
'serviceName',
'httpRoute',
'msgSystem',
'msgOperation',
'dbSystem',
'rpcSystem',
'rpcService',
'rpcMethod',
'peerService',
];
return (
deprecatedIntrinsicFields.includes(fieldName) ||
deprecatedCalculatedFields.includes(fieldName)
);
}
function createBaseSpec(
queryData: IBuilderQuery,
requestType: RequestType,
@@ -143,16 +177,29 @@ function createBaseSpec(
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,
}),
(column: any): TelemetryFieldKey => {
const fieldName = column.name ?? column.key;
const isDeprecated = isDeprecatedField(fieldName);
const fieldObj: TelemetryFieldKey = {
name: fieldName,
fieldDataType:
column?.fieldDataType ?? (column?.dataType as FieldDataType),
signal: column?.signal ?? undefined,
};
// Only add fieldContext if the field is NOT deprecated
if (!isDeprecated && fieldName !== 'name') {
fieldObj.fieldContext =
column?.fieldContext ?? (column?.type as FieldContext);
}
return fieldObj;
},
),
};
}
// Utility to parse aggregation expressions with optional alias
export function parseAggregations(
expression: string,
@@ -279,103 +326,6 @@ 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
*/
@@ -457,27 +407,14 @@ export const prepareQueryRangePayloadV5 = ({
switch (query.queryType) {
case EQueryType.QUERY_BUILDER: {
const { queryData: data, queryFormulas, queryTraceOperator } = query.builder;
const { queryData: data, queryFormulas } = query.builder;
const currentQueryData = mapQueryDataToApi(data, 'queryName', tableParams);
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
legendMap = {
...currentQueryData.newLegendMap,
...currentFormulas.newLegendMap,
...currentTraceOperator.newLegendMap,
};
// Convert builder queries
@@ -510,36 +447,8 @@ 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
queries = [...builderQueries, ...formulaQueries, ...traceOperatorQueries];
queries = [...builderQueries, ...formulaQueries];
break;
}
case EQueryType.PROM: {

View File

@@ -22,10 +22,6 @@
flex: 1;
position: relative;
.qb-trace-view-selector-container {
padding: 12px 8px 8px 8px;
}
}
.qb-content-section {
@@ -183,7 +179,7 @@
flex-direction: column;
gap: 8px;
margin-left: 26px;
margin-left: 32px;
padding-bottom: 16px;
padding-left: 8px;
@@ -199,8 +195,8 @@
}
.formula-container {
padding: 8px;
margin-left: 74px;
margin-left: 82px;
padding: 4px 0px;
.ant-col {
&::before {
@@ -335,12 +331,6 @@
);
left: 15px;
}
&.has-trace-operator {
&::before {
height: 0px;
}
}
}
.formula-name {
@@ -357,7 +347,7 @@
&::before {
content: '';
height: 128px;
height: 65px;
content: '';
position: absolute;
left: 0;

View File

@@ -5,13 +5,11 @@ import { Formula } from 'container/QueryBuilder/components/Formula';
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { memo, useEffect, useMemo, useRef } from 'react';
import { IBuilderTraceOperator } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { QueryBuilderV2Provider } from './QueryBuilderV2Context';
import QueryFooter from './QueryV2/QueryFooter/QueryFooter';
import { QueryV2 } from './QueryV2/QueryV2';
import TraceOperator from './QueryV2/TraceOperator/TraceOperator';
export const QueryBuilderV2 = memo(function QueryBuilderV2({
config,
@@ -20,7 +18,6 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
queryComponents,
isListViewPanel = false,
showOnlyWhereClause = false,
showTraceOperator = false,
version,
}: QueryBuilderProps): JSX.Element {
const {
@@ -28,7 +25,6 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
addNewBuilderQuery,
addNewFormula,
handleSetConfig,
addTraceOperator,
panelType,
initialDataSource,
} = useQueryBuilder();
@@ -58,14 +54,6 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
newPanelType,
]);
const isMultiQueryAllowed = useMemo(
() =>
!showOnlyWhereClause ||
!isListViewPanel ||
(currentDataSource === DataSource.TRACES && showTraceOperator),
[showOnlyWhereClause, currentDataSource, showTraceOperator, isListViewPanel],
);
const listViewLogFilterConfigs: QueryBuilderProps['filterConfigs'] = useMemo(() => {
const config: QueryBuilderProps['filterConfigs'] = {
stepInterval: { isHidden: true, isDisabled: true },
@@ -109,45 +97,11 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
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 (
<QueryBuilderV2Provider>
<div className="query-builder-v2">
<div className="qb-content-container">
{!isMultiQueryAllowed ? (
{isListViewPanel && (
<QueryV2
ref={containerRef}
key={currentQuery.builder.queryData[0].queryName}
@@ -155,15 +109,15 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
query={currentQuery.builder.queryData[0]}
filterConfigs={queryFilterConfigs}
queryComponents={queryComponents}
isMultiQueryAllowed={isMultiQueryAllowed}
showTraceOperator={shouldShowTraceOperator}
version={version}
isAvailableToDisable={false}
queryVariant={config?.queryVariant || 'dropdown'}
showOnlyWhereClause={showOnlyWhereClause}
isListViewPanel={isListViewPanel}
/>
) : (
)}
{!isListViewPanel &&
currentQuery.builder.queryData.map((query, index) => (
<QueryV2
ref={containerRef}
@@ -173,16 +127,13 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
filterConfigs={queryFilterConfigs}
queryComponents={queryComponents}
version={version}
isMultiQueryAllowed={isMultiQueryAllowed}
isAvailableToDisable={false}
showTraceOperator={shouldShowTraceOperator}
queryVariant={config?.queryVariant || 'dropdown'}
showOnlyWhereClause={showOnlyWhereClause}
isListViewPanel={isListViewPanel}
signalSource={config?.signalSource || ''}
/>
))
)}
))}
{!showOnlyWhereClause && currentQuery.builder.queryFormulas.length > 0 && (
<div className="qb-formulas-container">
@@ -207,25 +158,15 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
</div>
)}
{shouldShowFooter && (
{!showOnlyWhereClause && !isListViewPanel && (
<QueryFooter
showAddFormula={showFormula}
addNewBuilderQuery={addNewBuilderQuery}
addNewFormula={addNewFormula}
addTraceOperator={addTraceOperator}
showAddTraceOperator={showTraceOperator && !traceOperator}
/>
)}
{shouldShowTraceOperator && (
<TraceOperator
isListViewPanel={isListViewPanel}
traceOperator={traceOperator as IBuilderTraceOperator}
/>
)}
</div>
{isMultiQueryAllowed && (
{!showOnlyWhereClause && !isListViewPanel && (
<div className="query-names-section">
{currentQuery.builder.queryData.map((query) => (
<div key={query.queryName} className="query-name">

View File

@@ -1,11 +1,7 @@
.query-add-ons {
width: 100%;
}
.add-ons-list {
display: flex;
justify-content: space-between;
align-items: center;
gap: 16px;
.add-ons-tabs {
display: flex;

View File

@@ -144,8 +144,6 @@ function QueryAddOns({
showReduceTo,
panelType,
index,
isForTraceOperator = false,
children,
}: {
query: IBuilderQuery;
version: string;
@@ -153,8 +151,6 @@ function QueryAddOns({
showReduceTo: boolean;
panelType: PANEL_TYPES | null;
index: number;
isForTraceOperator?: boolean;
children?: React.ReactNode;
}): JSX.Element {
const [addOns, setAddOns] = useState<AddOn[]>(ADD_ONS);
@@ -164,7 +160,6 @@ function QueryAddOns({
index,
query,
entityVersion: '',
isForTraceOperator,
});
const { handleSetQueryData } = useQueryBuilder();
@@ -491,7 +486,6 @@ function QueryAddOns({
</Tooltip>
))}
</Radio.Group>
{children}
</div>
</div>
);

View File

@@ -4,10 +4,7 @@ import { Tooltip } from 'antd';
import InputWithLabel from 'components/InputWithLabel/InputWithLabel';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { useMemo } from 'react';
import {
IBuilderQuery,
IBuilderTraceOperator,
} from 'types/api/queryBuilder/queryBuilderData';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import QueryAggregationSelect from './QueryAggregationSelect';
@@ -23,7 +20,7 @@ function QueryAggregationOptions({
panelType?: string;
onAggregationIntervalChange: (value: number) => void;
onChange?: (value: string) => void;
queryData: IBuilderQuery | IBuilderTraceOperator;
queryData: IBuilderQuery;
}): JSX.Element {
const showAggregationInterval = useMemo(() => {
// eslint-disable-next-line sonarjs/prefer-single-boolean-return

View File

@@ -4,15 +4,9 @@ import { Plus, Sigma } from 'lucide-react';
export default function QueryFooter({
addNewBuilderQuery,
addNewFormula,
addTraceOperator,
showAddFormula = true,
showAddTraceOperator = false,
}: {
addNewBuilderQuery: () => void;
addNewFormula: () => void;
addTraceOperator?: () => void;
showAddTraceOperator: boolean;
showAddFormula?: boolean;
}): JSX.Element {
return (
<div className="qb-footer">
@@ -28,62 +22,32 @@ export default function QueryFooter({
</Tooltip>
</div>
{showAddFormula && (
<div className="qb-add-formula">
<Tooltip
title={
<div style={{ textAlign: 'center' }}>
Add New Formula
<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>
}
<div className="qb-add-formula">
<Tooltip
title={
<div style={{ textAlign: 'center' }}>
Add New Formula
<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={addNewFormula}
>
<Button
className="add-formula-button periscope-btn secondary"
icon={<Sigma size={16} />}
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>
)}
Add Formula
</Button>
</Tooltip>
</div>
</div>
</div>
);

View File

@@ -7,7 +7,6 @@
'Helvetica Neue', sans-serif;
.query-where-clause-editor-container {
position: relative;
display: flex;
flex-direction: row;

View File

@@ -26,11 +26,9 @@ export const QueryV2 = memo(function QueryV2({
query,
filterConfigs,
isListViewPanel = false,
showTraceOperator = false,
version,
showOnlyWhereClause = false,
signalSource = '',
isMultiQueryAllowed = false,
}: QueryProps & { ref: React.RefObject<HTMLDivElement> }): JSX.Element {
const { cloneQuery, panelType } = useQueryBuilder();
@@ -110,15 +108,11 @@ export const QueryV2 = memo(function QueryV2({
ref={ref}
>
<div className="qb-content-section">
{isMultiQueryAllowed && (
{!showOnlyWhereClause && (
<div className="qb-header-container">
<div className="query-actions-container">
<div className="query-actions-left-container">
<QBEntityOptions
hasTraceOperator={
showTraceOperator ||
(isListViewPanel && dataSource === DataSource.TRACES)
}
isMetricsDataSource={dataSource === DataSource.METRICS}
showFunctions={
(version && version === ENTITY_VERSION_V4) ||
@@ -145,30 +139,7 @@ export const QueryV2 = memo(function QueryV2({
/>
</div>
{!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 && (
{!isListViewPanel && (
<Dropdown
className="query-actions-dropdown"
menu={{
@@ -210,32 +181,28 @@ export const QueryV2 = memo(function QueryV2({
</div>
)}
{!showTraceOperator &&
!(isListViewPanel && dataSource === DataSource.TRACES) && (
<div className="qb-search-filter-container">
<div className="query-search-container">
<QuerySearch
key={`query-search-${query.queryName}-${query.dataSource}`}
onChange={handleSearchChange}
queryData={query}
dataSource={dataSource}
signalSource={signalSource}
/>
</div>
<div className="qb-search-filter-container">
<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>
)}
{showSpanScopeSelector && (
<div className="traces-search-filter-container">
<div className="traces-search-filter-in">in</div>
<SpanScopeSelector query={query} />
</div>
)}
</div>
</div>
{!showOnlyWhereClause &&
!isListViewPanel &&
!showTraceOperator &&
dataSource !== DataSource.METRICS && (
<QueryAggregation
dataSource={dataSource}
@@ -258,7 +225,7 @@ export const QueryV2 = memo(function QueryV2({
/>
)}
{!showOnlyWhereClause && !isListViewPanel && !showTraceOperator && (
{!showOnlyWhereClause && (
<QueryAddOns
index={index}
query={query}

View File

@@ -1,180 +0,0 @@
.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;
}
}
}
}

View File

@@ -1,157 +0,0 @@
/* 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>
);
}

View File

@@ -17,19 +17,6 @@
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 {
border: 1px solid var(--bg-slate-400);
&:hover {

View File

@@ -6,7 +6,6 @@ import { RadioChangeEvent } from 'antd/es/radio';
interface Option {
value: string;
label: string;
icon?: React.ReactNode;
}
interface SignozRadioGroupProps {
@@ -38,10 +37,7 @@ function SignozRadioGroup({
value={option.value}
className={value === option.value ? 'selected_view tab' : 'tab'}
>
<div className="view-title-container">
{option.icon && <div className="icon-container">{option.icon}</div>}
{option.label}
</div>
{option.label}
</Radio.Button>
))}
</Radio.Group>

View File

@@ -12,7 +12,6 @@ import {
HavingForm,
IBuilderFormula,
IBuilderQuery,
IBuilderTraceOperator,
IClickHouseQuery,
IPromQLQuery,
Query,
@@ -51,8 +50,6 @@ import {
export const MAX_FORMULAS = 20;
export const MAX_QUERIES = 26;
export const TRACE_OPERATOR_QUERY_NAME = 'T1';
export const idDivider = '--';
export const selectValueDivider = '__';
@@ -268,11 +265,6 @@ export const initialFormulaBuilderFormValues: IBuilderFormula = {
legend: '',
};
export const initialQueryBuilderFormTraceOperatorValues: IBuilderTraceOperator = {
...initialQueryBuilderFormTracesValues,
queryName: TRACE_OPERATOR_QUERY_NAME,
};
export const initialQueryPromQLData: IPromQLQuery = {
name: createNewBuilderItemName({ existNames: [], sourceNames: alphabet }),
query: '',
@@ -290,7 +282,6 @@ export const initialClickHouseData: IClickHouseQuery = {
export const initialQueryBuilderData: QueryBuilderData = {
queryData: [initialQueryBuilderFormValues],
queryFormulas: [],
queryTraceOperator: [],
};
export const initialSingleQueryMap: Record<

View File

@@ -54,7 +54,6 @@ function QuerySection({
queryVariant: 'static',
initialDataSource: ALERTS_DATA_SOURCE_MAP[alertType],
}}
showTraceOperator={alertType === AlertTypes.TRACES_BASED_ALERT}
showFunctions={
(alertType === AlertTypes.METRICS_BASED_ALERT &&
alertDef.version === ENTITY_VERSION_V4) ||

View File

@@ -150,7 +150,6 @@ function FormAlertRules({
const queryOptions = useMemo(() => {
const queryConfig: Record<EQueryType, () => SelectProps['options']> = {
// TODO: Filter out queries who are used in trace operator
[EQueryType.QUERY_BUILDER]: () => [
...(getSelectedQueryOptions(currentQuery.builder.queryData) || []),
...(getSelectedQueryOptions(currentQuery.builder.queryFormulas) || []),

View File

@@ -23,7 +23,7 @@ export const flattenLabels = (labels: Labels): ILabelRecord[] => {
if (!hiddenLabels.includes(key)) {
recs.push({
key,
value: labels[key],
value: labels[key] || '',
});
}
});

View File

@@ -272,12 +272,11 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
width: 80,
key: 'severity',
sorter: (a, b): number =>
(a.labels ? a.labels.severity.length : 0) -
(b.labels ? b.labels.severity.length : 0),
(a?.labels?.severity?.length || 0) - (b?.labels?.severity?.length || 0),
render: (value): JSX.Element => {
const objectKeys = Object.keys(value);
const objectKeys = value ? Object.keys(value) : [];
const withSeverityKey = objectKeys.find((e) => e === 'severity') || '';
const severityValue = value[withSeverityKey];
const severityValue = withSeverityKey ? value[withSeverityKey] : '-';
return <Typography>{severityValue}</Typography>;
},
@@ -290,7 +289,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
align: 'center',
width: 100,
render: (value): JSX.Element => {
const objectKeys = Object.keys(value);
const objectKeys = value ? Object.keys(value) : [];
const withOutSeverityKeys = objectKeys.filter((e) => e !== 'severity');
if (withOutSeverityKeys.length === 0) {

View File

@@ -16,6 +16,12 @@
padding: 20px;
gap: 36px;
.info {
display: flex;
flex-direction: column;
gap: 12px;
}
.meter-column-graph {
.row-card {
background-color: var(--bg-ink-400);

View File

@@ -1,6 +1,6 @@
import './BreakDown.styles.scss';
import { Typography } from 'antd';
import { Alert, Typography } from 'antd';
// import useFilterConfig from 'components/QuickFilters/hooks/useFilterConfig';
// import { SignalType } from 'components/QuickFilters/types';
import { QueryParams } from 'constants/query';
@@ -171,6 +171,19 @@ function BreakDown(): JSX.Element {
<DateTimeSelectionV2 showAutoRefresh={false} />
</section>
<section className="meter-explorer-graphs">
<section className="info">
<Alert
type="info"
showIcon
message="Billing is calculated in UTC. To match your meter data with billing, select full-day ranges in UTC time (00:00 23:59 UTC).
For example, if youre in IST, for the billing of Jan 1, select your time range as Jan 1, 5:30 AM Jan 2, 5:29 AM IST."
/>
<Alert
type="warning"
showIcon
message="Meter module data is accurate only from 22nd August 2025, 00:00 UTC onwards. Data before this time was collected during the beta phase and may be inaccurate."
/>
</section>
<section className="total">
<Section
id={sections[0].id}

View File

@@ -30,14 +30,5 @@ export type QueryBuilderProps = {
isListViewPanel?: boolean;
showFunctions?: boolean;
showOnlyWhereClause?: boolean;
showOnlyTraceOperator?: boolean;
showTraceViewSelector?: boolean;
showTraceOperator?: boolean;
version: string;
onChangeTraceView?: (view: TraceView) => void;
};
export enum TraceView {
SPANS = 'spans',
TRACES = 'traces',
}

View File

@@ -39,7 +39,6 @@ interface QBEntityOptionsProps {
showCloneOption?: boolean;
isListViewPanel?: boolean;
index?: number;
hasTraceOperator?: boolean;
queryVariant?: 'dropdown' | 'static';
onChangeDataSource?: (value: DataSource) => void;
}
@@ -62,7 +61,6 @@ export default function QBEntityOptions({
onCloneQuery,
index,
queryVariant,
hasTraceOperator = false,
onChangeDataSource,
}: QBEntityOptionsProps): JSX.Element {
const handleCloneEntity = (): void => {
@@ -99,7 +97,7 @@ export default function QBEntityOptions({
value="query-builder"
className="periscope-btn visibility-toggle"
onClick={onToggleVisibility}
disabled={isListViewPanel && query?.dataSource !== DataSource.TRACES}
disabled={isListViewPanel}
>
{entityData.disabled ? <EyeOff size={16} /> : <Eye size={16} />}
</Button>
@@ -117,7 +115,6 @@ export default function QBEntityOptions({
className={cx(
'periscope-btn',
entityType === 'query' ? 'query-name' : 'formula-name',
hasTraceOperator && 'has-trace-operator',
isLogsExplorerPage && lastUsedQuery === index ? 'sync-btn' : '',
)}
>

View File

@@ -11,7 +11,5 @@ export type QueryProps = {
version: string;
showSpanScopeSelector?: boolean;
showOnlyWhereClause?: boolean;
showTraceOperator?: boolean;
signalSource?: string;
isMultiQueryAllowed?: boolean;
} & Pick<QueryBuilderProps, 'filterConfigs' | 'queryComponents'>;

View File

@@ -270,7 +270,7 @@ export const defaultMoreMenuItems: SidebarItem[] = [
label: 'Cost Meter',
icon: <ChartArea size={16} />,
isNew: false,
isEnabled: false,
isEnabled: true,
isBeta: true,
itemKey: 'meter-explorer',
},

View File

@@ -37,15 +37,11 @@ function QuerySection(): JSX.Element {
};
}, [panelTypes, renderOrderBy]);
const isListViewPanel = useMemo(
() => panelTypes === PANEL_TYPES.LIST || panelTypes === PANEL_TYPES.TRACE,
[panelTypes],
);
return (
<QueryBuilderV2
isListViewPanel={isListViewPanel}
showTraceOperator
isListViewPanel={
panelTypes === PANEL_TYPES.LIST || panelTypes === PANEL_TYPES.TRACE
}
config={{ initialDataSource: DataSource.TRACES, queryVariant: 'static' }}
queryComponents={queryComponents}
panelType={panelTypes}

View File

@@ -54,11 +54,9 @@ export const useQueryOperations: UseQueryOperations = ({
formula,
isListViewPanel = false,
entityVersion,
isForTraceOperator = false,
}) => {
const {
handleSetQueryData,
handleSetTraceOperatorData,
handleSetFormulaData,
removeQueryBuilderEntityByIndex,
panelType,
@@ -402,19 +400,9 @@ export const useQueryOperations: UseQueryOperations = ({
: value,
};
if (isForTraceOperator) {
handleSetTraceOperatorData(index, newQuery);
} else {
handleSetQueryData(index, newQuery);
}
handleSetQueryData(index, newQuery);
},
[
query,
index,
handleSetQueryData,
handleSetTraceOperatorData,
isForTraceOperator,
],
[query, index, handleSetQueryData],
);
const handleChangeFormulaData: HandleChangeFormulaData = useCallback(

View File

@@ -14,7 +14,7 @@ export type AlertHeaderProps = {
state: string;
alert: string;
id: string;
labels: Record<string, string>;
labels: Record<string, string | undefined> | undefined;
disabled: boolean;
};
};
@@ -23,13 +23,14 @@ function AlertHeader({ alertDetails }: AlertHeaderProps): JSX.Element {
const { alertRuleState } = useAlertRule();
const [updatedName, setUpdatedName] = useState(alertName);
const labelsWithoutSeverity = useMemo(
() =>
Object.fromEntries(
const labelsWithoutSeverity = useMemo(() => {
if (labels) {
return Object.fromEntries(
Object.entries(labels).filter(([key]) => key !== 'severity'),
),
[labels],
);
);
}
return {};
}, [labels]);
return (
<div className="alert-info">
@@ -43,7 +44,7 @@ function AlertHeader({ alertDetails }: AlertHeaderProps): JSX.Element {
</div>
</div>
<div className="bottom-section">
{labels.severity && <AlertSeverity severity={labels.severity} />}
{labels?.severity && <AlertSeverity severity={labels.severity} />}
{/* // TODO(shaheer): Get actual data when we are able to get alert firing from state from API */}
{/* <AlertStatus

View File

@@ -53,6 +53,7 @@ function TracesExplorer(): JSX.Element {
handleRunQuery,
stagedQuery,
handleSetConfig,
updateQueriesData,
} = useQueryBuilder();
const { options } = useOptionsMenu({
@@ -111,14 +112,48 @@ function TracesExplorer(): JSX.Element {
handleSetConfig(PANEL_TYPES.LIST, DataSource.TRACES);
}
// TODO: remove formula when switching to List view
if (view === ExplorerViews.LIST) {
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);
handleExplorerTabChange(
view === ExplorerViews.TIMESERIES ? PANEL_TYPES.TIME_SERIES : view,
);
},
[handleSetConfig, handleExplorerTabChange, selectedView, setSelectedView],
[
handleSetConfig,
handleExplorerTabChange,
selectedView,
currentQuery,
updateAllQueriesOperators,
updateQueriesData,
setSelectedView,
],
);
const listQuery = useMemo(() => {

View File

@@ -7,7 +7,6 @@ import {
initialClickHouseData,
initialFormulaBuilderFormValues,
initialQueriesMap,
initialQueryBuilderFormTraceOperatorValues,
initialQueryBuilderFormValuesMap,
initialQueryPromQLData,
initialQueryState,
@@ -15,7 +14,6 @@ import {
MAX_FORMULAS,
MAX_QUERIES,
PANEL_TYPES,
TRACE_OPERATOR_QUERY_NAME,
} from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import {
@@ -49,7 +47,6 @@ import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteRe
import {
IBuilderFormula,
IBuilderQuery,
IBuilderTraceOperator,
IClickHouseQuery,
IPromQLQuery,
Query,
@@ -78,18 +75,14 @@ export const QueryBuilderContext = createContext<QueryBuilderContextType>({
panelType: PANEL_TYPES.TIME_SERIES,
isEnabledQuery: false,
handleSetQueryData: () => {},
handleSetTraceOperatorData: () => {},
handleSetFormulaData: () => {},
handleSetQueryItemData: () => {},
handleSetConfig: () => {},
removeQueryBuilderEntityByIndex: () => {},
removeAllQueryBuilderEntities: () => {},
removeQueryTypeItemByIndex: () => {},
addNewBuilderQuery: () => {},
cloneQuery: () => {},
addNewFormula: () => {},
addTraceOperator: () => {},
removeTraceOperator: () => {},
addNewQueryItem: () => {},
redirectWithQueryBuilderData: () => {},
handleRunQuery: () => {},
@@ -180,10 +173,6 @@ export function QueryBuilderProvider({
...initialFormulaBuilderFormValues,
...item,
})),
queryTraceOperator: query.builder.queryTraceOperator?.map((item) => ({
...initialQueryBuilderFormTraceOperatorValues,
...item,
})),
};
const setupedQueryData = builder.queryData.map((item) => {
@@ -396,11 +385,8 @@ export function QueryBuilderProvider({
const removeQueryBuilderEntityByIndex = useCallback(
(type: keyof QueryBuilderData, index: number) => {
setCurrentQuery((prevState) => {
const currentArray: (
| IBuilderQuery
| IBuilderFormula
| IBuilderTraceOperator
)[] = prevState.builder[type];
const currentArray: (IBuilderQuery | IBuilderFormula)[] =
prevState.builder[type];
const filteredArray = currentArray.filter((_, i) => index !== i);
@@ -414,11 +400,8 @@ export function QueryBuilderProvider({
});
// eslint-disable-next-line sonarjs/no-identical-functions
setSupersetQuery((prevState) => {
const currentArray: (
| IBuilderQuery
| IBuilderFormula
| IBuilderTraceOperator
)[] = prevState.builder[type];
const currentArray: (IBuilderQuery | IBuilderFormula)[] =
prevState.builder[type];
const filteredArray = currentArray.filter((_, i) => index !== i);
@@ -434,20 +417,6 @@ 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(
(type: EQueryType.PROM | EQueryType.CLICKHOUSE, index: number) => {
setCurrentQuery((prevState) => {
@@ -670,68 +639,6 @@ export function QueryBuilderProvider({
});
}, [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>(
arr: T[],
index: number,
@@ -838,44 +745,6 @@ export function QueryBuilderProvider({
},
[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(
(index: number, formulaData: IBuilderFormula): void => {
setCurrentQuery((prevState) => {
@@ -1176,18 +1045,14 @@ export function QueryBuilderProvider({
panelType,
isEnabledQuery,
handleSetQueryData,
handleSetTraceOperatorData,
handleSetFormulaData,
handleSetQueryItemData,
handleSetConfig,
removeQueryBuilderEntityByIndex,
removeQueryTypeItemByIndex,
removeAllQueryBuilderEntities,
cloneQuery,
addNewBuilderQuery,
addNewFormula,
addTraceOperator,
removeTraceOperator,
addNewQueryItem,
redirectWithQueryBuilderData,
handleRunQuery,
@@ -1208,18 +1073,14 @@ export function QueryBuilderProvider({
panelType,
isEnabledQuery,
handleSetQueryData,
handleSetTraceOperatorData,
handleSetFormulaData,
handleSetQueryItemData,
handleSetConfig,
removeQueryBuilderEntityByIndex,
removeQueryTypeItemByIndex,
removeAllQueryBuilderEntities,
cloneQuery,
addNewBuilderQuery,
addNewFormula,
addTraceOperator,
removeTraceOperator,
addNewQueryItem,
redirectWithQueryBuilderData,
handleRunQuery,

View File

@@ -48,7 +48,7 @@ export interface RuleCondition {
seasonality?: string;
}
export interface Labels {
[key: string]: string;
[key: string]: string | undefined;
}
export interface AlertRuleStats {

View File

@@ -29,10 +29,6 @@ export interface IBuilderFormula {
orderBy?: OrderByPayload[];
}
export type IBuilderTraceOperator = IBuilderQuery & {
returnSpansFrom?: string;
};
export interface TagFilterItem {
id: string;
key?: BaseAutocompleteData;
@@ -128,7 +124,6 @@ export type BuilderQueryDataResourse = Record<
export type MapData =
| IBuilderQuery
| IBuilderFormula
| IBuilderTraceOperator
| IClickHouseQuery
| IPromQLQuery;

View File

@@ -14,7 +14,6 @@ export type RequestType =
export type QueryType =
| 'builder_query'
| 'builder_trace_operator'
| 'builder_formula'
| 'builder_sub_query'
| 'builder_join'

View File

@@ -4,7 +4,6 @@ import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteRe
import {
IBuilderFormula,
IBuilderQuery,
IBuilderTraceOperator,
} from 'types/api/queryBuilder/queryBuilderData';
import {
BaseBuilderQuery,
@@ -19,7 +18,6 @@ import { SelectOption } from './select';
type UseQueryOperationsParams = Pick<QueryProps, 'index' | 'query'> &
Pick<QueryBuilderProps, 'filterConfigs'> & {
isForTraceOperator?: boolean;
formula?: IBuilderFormula;
isListViewPanel?: boolean;
entityVersion: string;
@@ -34,14 +32,6 @@ export type HandleChangeQueryData<T = IBuilderQuery> = <
value: Value,
) => void;
export type HandleChangeTraceOperatorData<T = IBuilderTraceOperator> = <
Key extends keyof T,
Value extends T[Key]
>(
key: Key,
value: Value,
) => void;
// Legacy version for backward compatibility
export type HandleChangeQueryDataLegacy = HandleChangeQueryData<IBuilderQuery>;

View File

@@ -6,7 +6,6 @@ import { Dispatch, SetStateAction } from 'react';
import {
IBuilderFormula,
IBuilderQuery,
IBuilderTraceOperator,
IClickHouseQuery,
IPromQLQuery,
Query,
@@ -223,7 +222,6 @@ export type ReduceOperators = 'last' | 'sum' | 'avg' | 'max' | 'min';
export type QueryBuilderData = {
queryData: IBuilderQuery[];
queryFormulas: IBuilderFormula[];
queryTraceOperator: IBuilderTraceOperator[];
};
export type QueryBuilderContextType = {
@@ -237,10 +235,6 @@ export type QueryBuilderContextType = {
panelType: PANEL_TYPES | null;
isEnabledQuery: boolean;
handleSetQueryData: (index: number, queryData: IBuilderQuery) => void;
handleSetTraceOperatorData: (
index: number,
traceOperatorData: IBuilderTraceOperator,
) => void;
handleSetFormulaData: (index: number, formulaData: IBuilderFormula) => void;
handleSetQueryItemData: (
index: number,
@@ -255,15 +249,12 @@ export type QueryBuilderContextType = {
type: keyof QueryBuilderData,
index: number,
) => void;
removeAllQueryBuilderEntities: (type: keyof QueryBuilderData) => void;
removeQueryTypeItemByIndex: (
type: EQueryType.PROM | EQueryType.CLICKHOUSE,
index: number,
) => void;
addNewBuilderQuery: () => void;
addNewFormula: () => void;
removeTraceOperator: () => void;
addTraceOperator: (expression?: string) => void;
cloneQuery: (type: string, query: IBuilderQuery) => void;
addNewQueryItem: (type: EQueryType.PROM | EQueryType.CLICKHOUSE) => void;
redirectWithQueryBuilderData: (

View File

@@ -42,10 +42,8 @@ func consume(rows driver.Rows, kind qbtypes.RequestType, queryWindow *qbtypes.Ti
payload, err = readAsTimeSeries(rows, queryWindow, step, queryName)
case qbtypes.RequestTypeScalar:
payload, err = readAsScalar(rows, queryName)
case qbtypes.RequestTypeRaw:
case qbtypes.RequestTypeRaw, qbtypes.RequestTypeTrace:
payload, err = readAsRaw(rows, queryName)
case qbtypes.RequestTypeTrace:
payload, err = readAsTrace(rows, queryName)
// TODO: add support for other request types
}
@@ -334,74 +332,6 @@ func readAsScalar(rows driver.Rows, queryName string) (*qbtypes.ScalarData, erro
}, 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 {
if v == nil {
return nil

View File

@@ -29,8 +29,6 @@ func getqueryInfo(spec any) queryInfo {
return queryInfo{Name: s.Name, Disabled: s.Disabled, Step: s.StepInterval}
case qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]:
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:
return queryInfo{Name: s.Name, Disabled: s.Disabled}
case qbtypes.PromQuery:
@@ -72,10 +70,6 @@ func (q *querier) postProcessResults(ctx context.Context, results map[string]any
result = postProcessMetricQuery(q, result, spec, req)
typedResults[spec.Name] = result
}
case qbtypes.QueryBuilderTraceOperator:
if result, ok := typedResults[spec.Name]; ok {
typedResults[spec.Name] = result
}
}
}

View File

@@ -28,16 +28,15 @@ var (
)
type querier struct {
logger *slog.Logger
telemetryStore telemetrystore.TelemetryStore
metadataStore telemetrytypes.MetadataStore
promEngine prometheus.Prometheus
traceStmtBuilder qbtypes.StatementBuilder[qbtypes.TraceAggregation]
logStmtBuilder qbtypes.StatementBuilder[qbtypes.LogAggregation]
metricStmtBuilder qbtypes.StatementBuilder[qbtypes.MetricAggregation]
meterStmtBuilder qbtypes.StatementBuilder[qbtypes.MetricAggregation]
traceOperatorStmtBuilder qbtypes.TraceOperatorStatementBuilder
bucketCache BucketCache
logger *slog.Logger
telemetryStore telemetrystore.TelemetryStore
metadataStore telemetrytypes.MetadataStore
promEngine prometheus.Prometheus
traceStmtBuilder qbtypes.StatementBuilder[qbtypes.TraceAggregation]
logStmtBuilder qbtypes.StatementBuilder[qbtypes.LogAggregation]
metricStmtBuilder qbtypes.StatementBuilder[qbtypes.MetricAggregation]
meterStmtBuilder qbtypes.StatementBuilder[qbtypes.MetricAggregation]
bucketCache BucketCache
}
var _ Querier = (*querier)(nil)
@@ -51,21 +50,19 @@ func New(
logStmtBuilder qbtypes.StatementBuilder[qbtypes.LogAggregation],
metricStmtBuilder qbtypes.StatementBuilder[qbtypes.MetricAggregation],
meterStmtBuilder qbtypes.StatementBuilder[qbtypes.MetricAggregation],
traceOperatorStmtBuilder qbtypes.TraceOperatorStatementBuilder,
bucketCache BucketCache,
) *querier {
querierSettings := factory.NewScopedProviderSettings(settings, "github.com/SigNoz/signoz/pkg/querier")
return &querier{
logger: querierSettings.Logger(),
telemetryStore: telemetryStore,
metadataStore: metadataStore,
promEngine: promEngine,
traceStmtBuilder: traceStmtBuilder,
logStmtBuilder: logStmtBuilder,
metricStmtBuilder: metricStmtBuilder,
meterStmtBuilder: meterStmtBuilder,
traceOperatorStmtBuilder: traceOperatorStmtBuilder,
bucketCache: bucketCache,
logger: querierSettings.Logger(),
telemetryStore: telemetryStore,
metadataStore: metadataStore,
promEngine: promEngine,
traceStmtBuilder: traceStmtBuilder,
logStmtBuilder: logStmtBuilder,
metricStmtBuilder: metricStmtBuilder,
meterStmtBuilder: meterStmtBuilder,
bucketCache: bucketCache,
}
}
@@ -127,28 +124,9 @@ func (q *querier) QueryRange(ctx context.Context, orgID valuer.UUID, req *qbtype
NumberOfQueries: len(req.CompositeQuery.Queries),
PanelType: req.RequestType.StringValue(),
}
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
metricNames := make([]string, 0)
for idx, query := range req.CompositeQuery.Queries {
@@ -242,21 +220,6 @@ func (q *querier) QueryRange(ctx context.Context, orgID valuer.UUID, req *qbtype
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
}
}
}
@@ -277,38 +240,6 @@ func (q *querier) QueryRange(ctx context.Context, orgID valuer.UUID, req *qbtype
steps := make(map[string]qbtypes.Step)
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 {
case qbtypes.QueryTypePromQL:
promQuery, ok := query.Spec.(qbtypes.PromQuery)
@@ -325,22 +256,6 @@ 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)
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:
switch spec := query.Spec.(type) {
case qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]:
@@ -663,16 +578,7 @@ 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.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:
return nil
}

View File

@@ -89,16 +89,6 @@ func newProvider(
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
logFieldMapper := telemetrylogs.NewFieldMapper()
logConditionBuilder := telemetrylogs.NewConditionBuilder(logFieldMapper)
@@ -157,7 +147,7 @@ func newProvider(
cfg.FluxInterval,
)
// Create and return the querier - ADD traceOperatorStmtBuilder parameter
// Create and return the querier
return querier.New(
settings,
telemetryStore,
@@ -167,7 +157,6 @@ func newProvider(
logStmtBuilder,
metricStmtBuilder,
meterStmtBuilder,
traceOperatorStmtBuilder,
bucketCache,
), nil
}

View File

@@ -1,145 +0,0 @@
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
}

View File

@@ -64,6 +64,7 @@ import (
"github.com/SigNoz/signoz/pkg/types/licensetypes"
"github.com/SigNoz/signoz/pkg/types/opamptypes"
"github.com/SigNoz/signoz/pkg/types/pipelinetypes"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
ruletypes "github.com/SigNoz/signoz/pkg/types/ruletypes"
traceFunnels "github.com/SigNoz/signoz/pkg/types/tracefunneltypes"
@@ -1035,9 +1036,54 @@ func (aH *APIHandler) getRuleStateHistory(w http.ResponseWriter, r *http.Request
// to get the correct query range
start := end.Add(-time.Duration(rule.EvalWindow)).Add(-3 * time.Minute)
if rule.AlertType == ruletypes.AlertTypeLogs {
res.Items[idx].RelatedLogsLink = contextlinks.PrepareLinksToLogs(start, end, newFilters)
if rule.Version != "v5" {
res.Items[idx].RelatedLogsLink = contextlinks.PrepareLinksToLogs(start, end, newFilters)
} else {
// TODO(srikanthccv): re-visit this and support multiple queries
var q qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]
for _, query := range rule.RuleCondition.CompositeQuery.Queries {
if query.Type == qbtypes.QueryTypeBuilder {
switch spec := query.Spec.(type) {
case qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]:
q = spec
}
}
}
filterExpr := ""
if q.Filter != nil && q.Filter.Expression != "" {
filterExpr = q.Filter.Expression
}
whereClause := contextlinks.PrepareFilterExpression(lbls, filterExpr, q.GroupBy)
res.Items[idx].RelatedLogsLink = contextlinks.PrepareLinksToLogsV5(start, end, whereClause)
}
} else if rule.AlertType == ruletypes.AlertTypeTraces {
res.Items[idx].RelatedTracesLink = contextlinks.PrepareLinksToTraces(start, end, newFilters)
if rule.Version != "v5" {
res.Items[idx].RelatedTracesLink = contextlinks.PrepareLinksToTraces(start, end, newFilters)
} else {
// TODO(srikanthccv): re-visit this and support multiple queries
var q qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]
for _, query := range rule.RuleCondition.CompositeQuery.Queries {
if query.Type == qbtypes.QueryTypeBuilder {
switch spec := query.Spec.(type) {
case qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]:
q = spec
}
}
}
filterExpr := ""
if q.Filter != nil && q.Filter.Expression != "" {
filterExpr = q.Filter.Expression
}
whereClause := contextlinks.PrepareFilterExpression(lbls, filterExpr, q.GroupBy)
res.Items[idx].RelatedTracesLink = contextlinks.PrepareLinksToTracesV5(start, end, whereClause)
}
}
}
}

View File

@@ -478,6 +478,11 @@ func (r *ThresholdRule) buildAndRunQuery(ctx context.Context, orgID valuer.UUID,
return resultVector, nil
}
if queryResult == nil {
r.logger.WarnContext(ctx, "query result is nil", "rule_name", r.Name(), "query_name", selectedQuery)
return resultVector, nil
}
for _, series := range queryResult.Series {
smpl, shouldAlert := r.ShouldAlert(*series)
if shouldAlert {

View File

@@ -6,6 +6,7 @@ import (
"fmt"
"math"
"reflect"
"regexp"
"strconv"
"strings"
@@ -185,3 +186,11 @@ func FormatValueForContains(value any) string {
}
}
}
func FormatFullTextSearch(input string) string {
if _, err := regexp.Compile(input); err != nil {
// Not a valid regex -> treat as literal substring
return regexp.QuoteMeta(input)
}
return input
}

View File

@@ -273,3 +273,30 @@ func TestFormatValueForContains_LargeNumberScientificNotation(t *testing.T) {
assert.Equal(t, "521509198310", result)
assert.NotEqual(t, "5.2150919831e+11", result)
}
func TestFormatFullTextSearch(t *testing.T) {
tests := []struct {
input string
expected string
}{
// valid regex, unchanged
{"foo.*bar", "foo.*bar"},
// invalid regex, escaped
{"[ERROR-1234]", `\[ERROR-1234\]`},
// literal with +
{"C++ Error", `C\+\+ Error`},
// IP address, valid regex but unsafe chars
{"10.0.0.1", "10.0.0.1"},
// java class, '.' will still be regex wildcard
{"java.lang.NullPointerException", "java.lang.NullPointerException"},
// a-o invalid character class range
{"[LocalLog partition=__cluster_metadata-0,", "\\[LocalLog partition=__cluster_metadata-0,"},
{"[abcd]", "[abcd]"},
}
for _, tt := range tests {
got := FormatFullTextSearch(tt.input)
if got != tt.expected {
t.Errorf("FormatFullTextSearch(%q) = %q, want %q", tt.input, got, tt.expected)
}
}
}

View File

@@ -298,8 +298,9 @@ func (v *filterExpressionVisitor) VisitPrimary(ctx *grammar.PrimaryContext) any
child := ctx.GetChild(0)
if keyCtx, ok := child.(*grammar.KeyContext); ok {
// create a full text search condition on the body field
keyText := keyCtx.GetText()
cond, err := v.conditionBuilder.ConditionFor(context.Background(), v.fullTextColumn, qbtypes.FilterOperatorRegexp, keyText, v.builder)
cond, err := v.conditionBuilder.ConditionFor(context.Background(), v.fullTextColumn, qbtypes.FilterOperatorRegexp, FormatFullTextSearch(keyText), v.builder)
if err != nil {
v.errors = append(v.errors, fmt.Sprintf("failed to build full text search condition: %s", err.Error()))
return ""
@@ -319,7 +320,7 @@ func (v *filterExpressionVisitor) VisitPrimary(ctx *grammar.PrimaryContext) any
v.errors = append(v.errors, fmt.Sprintf("unsupported value type: %s", valCtx.GetText()))
return ""
}
cond, err := v.conditionBuilder.ConditionFor(context.Background(), v.fullTextColumn, qbtypes.FilterOperatorRegexp, text, v.builder)
cond, err := v.conditionBuilder.ConditionFor(context.Background(), v.fullTextColumn, qbtypes.FilterOperatorRegexp, FormatFullTextSearch(text), v.builder)
if err != nil {
v.errors = append(v.errors, fmt.Sprintf("failed to build full text search condition: %s", err.Error()))
return ""
@@ -594,7 +595,7 @@ func (v *filterExpressionVisitor) VisitFullText(ctx *grammar.FullTextContext) an
v.errors = append(v.errors, "full text search is not supported")
return ""
}
cond, err := v.conditionBuilder.ConditionFor(context.Background(), v.fullTextColumn, qbtypes.FilterOperatorRegexp, text, v.builder)
cond, err := v.conditionBuilder.ConditionFor(context.Background(), v.fullTextColumn, qbtypes.FilterOperatorRegexp, FormatFullTextSearch(text), v.builder)
if err != nil {
v.errors = append(v.errors, fmt.Sprintf("failed to build full text search condition: %s", err.Error()))
return ""

View File

@@ -64,6 +64,10 @@ func (c *conditionBuilder) conditionFor(
return sb.ILike(tblFieldName, value), nil
case qbtypes.FilterOperatorNotLike:
return sb.NotILike(tblFieldName, value), nil
case qbtypes.FilterOperatorRegexp:
return fmt.Sprintf(`match(LOWER(%s), LOWER(%s))`, tblFieldName, sb.Var(value)), nil
case qbtypes.FilterOperatorNotRegexp:
return fmt.Sprintf(`NOT match(LOWER(%s), LOWER(%s))`, tblFieldName, sb.Var(value)), nil
}
}

View File

@@ -60,7 +60,6 @@ func NewFieldMapper() qbtypes.FieldMapper {
}
func (m *fieldMapper) getColumn(_ context.Context, key *telemetrytypes.TelemetryFieldKey) (*schema.Column, error) {
switch key.FieldContext {
case telemetrytypes.FieldContextResource:
return logsV2Columns["resources_string"], nil

View File

@@ -41,17 +41,25 @@ func TestFilterExprLogs(t *testing.T) {
// Single word searches
{
category: "Single word",
query: "download",
query: "Download",
shouldPass: true,
expectedQuery: "WHERE match(body, ?)",
expectedArgs: []any{"download"},
expectedQuery: "WHERE match(LOWER(body), LOWER(?))",
expectedArgs: []any{"Download"},
expectedErrorContains: "",
},
{
category: "Single word invalid regex",
query: "'[LocalLog partition=__cluster_metadata-0,'",
shouldPass: true,
expectedQuery: "WHERE match(LOWER(body), LOWER(?))",
expectedArgs: []any{"\\[LocalLog partition=__cluster_metadata-0,"},
expectedErrorContains: "",
},
{
category: "Single word",
query: "LAMBDA",
shouldPass: true,
expectedQuery: "WHERE match(body, ?)",
expectedQuery: "WHERE match(LOWER(body), LOWER(?))",
expectedArgs: []any{"LAMBDA"},
expectedErrorContains: "",
},
@@ -59,7 +67,7 @@ func TestFilterExprLogs(t *testing.T) {
category: "Single word",
query: "AccessDenied",
shouldPass: true,
expectedQuery: "WHERE match(body, ?)",
expectedQuery: "WHERE match(LOWER(body), LOWER(?))",
expectedArgs: []any{"AccessDenied"},
expectedErrorContains: "",
},
@@ -67,7 +75,7 @@ func TestFilterExprLogs(t *testing.T) {
category: "Single word",
query: "42069",
shouldPass: true,
expectedQuery: "WHERE match(body, ?)",
expectedQuery: "WHERE match(LOWER(body), LOWER(?))",
expectedArgs: []any{"42069"},
expectedErrorContains: "",
},
@@ -75,7 +83,7 @@ func TestFilterExprLogs(t *testing.T) {
category: "Single word",
query: "pulljob",
shouldPass: true,
expectedQuery: "WHERE match(body, ?)",
expectedQuery: "WHERE match(LOWER(body), LOWER(?))",
expectedArgs: []any{"pulljob"},
expectedErrorContains: "",
},
@@ -91,7 +99,7 @@ func TestFilterExprLogs(t *testing.T) {
category: "Single word with spaces",
query: `" 504 "`,
shouldPass: true,
expectedQuery: "WHERE match(body, ?)",
expectedQuery: "WHERE match(LOWER(body), LOWER(?))",
expectedArgs: []any{" 504 "},
expectedErrorContains: "",
},
@@ -99,7 +107,7 @@ func TestFilterExprLogs(t *testing.T) {
category: "Single word with spaces",
query: `"Importing "`,
shouldPass: true,
expectedQuery: "WHERE match(body, ?)",
expectedQuery: "WHERE match(LOWER(body), LOWER(?))",
expectedArgs: []any{"Importing "},
expectedErrorContains: "",
},
@@ -107,7 +115,7 @@ func TestFilterExprLogs(t *testing.T) {
category: "Single word with spaces",
query: `"Job ID"`,
shouldPass: true,
expectedQuery: "WHERE match(body, ?)",
expectedQuery: "WHERE match(LOWER(body), LOWER(?))",
expectedArgs: []any{"Job ID"},
expectedErrorContains: "",
},
@@ -123,7 +131,7 @@ func TestFilterExprLogs(t *testing.T) {
category: "Special characters",
query: "srikanth@signoz.io",
shouldPass: true,
expectedQuery: "WHERE match(body, ?)",
expectedQuery: "WHERE match(LOWER(body), LOWER(?))",
expectedArgs: []any{"srikanth@signoz.io"},
expectedErrorContains: "",
},
@@ -131,7 +139,7 @@ func TestFilterExprLogs(t *testing.T) {
category: "Special characters",
query: "cancel_membership",
shouldPass: true,
expectedQuery: "WHERE match(body, ?)",
expectedQuery: "WHERE match(LOWER(body), LOWER(?))",
expectedArgs: []any{"cancel_membership"},
expectedErrorContains: "",
},
@@ -139,7 +147,7 @@ func TestFilterExprLogs(t *testing.T) {
category: "Special characters",
query: `"ERROR: cannot execute update() in a read-only context"`,
shouldPass: true,
expectedQuery: "WHERE match(body, ?)",
expectedQuery: "WHERE match(LOWER(body), LOWER(?))",
expectedArgs: []any{"ERROR: cannot execute update() in a read-only context"},
expectedErrorContains: "",
},
@@ -153,7 +161,7 @@ func TestFilterExprLogs(t *testing.T) {
category: "Special characters",
query: "https://example.com/user/default/0196877a-f01f-785e-a937-5da0a3efbb75",
shouldPass: true,
expectedQuery: "WHERE match(body, ?)",
expectedQuery: "WHERE match(LOWER(body), LOWER(?))",
expectedArgs: []any{"https://example.com/user/default/0196877a-f01f-785e-a937-5da0a3efbb75"},
expectedErrorContains: "",
},
@@ -161,7 +169,7 @@ func TestFilterExprLogs(t *testing.T) {
category: "Special characters",
query: "\"STEPS_PER_DAY\"",
shouldPass: true,
expectedQuery: "WHERE match(body, ?)",
expectedQuery: "WHERE match(LOWER(body), LOWER(?))",
expectedArgs: []any{"STEPS_PER_DAY"},
expectedErrorContains: "",
},
@@ -169,7 +177,7 @@ func TestFilterExprLogs(t *testing.T) {
category: "Special characters",
query: "#bvn",
shouldPass: true,
expectedQuery: "WHERE match(body, ?)",
expectedQuery: "WHERE match(LOWER(body), LOWER(?))",
expectedArgs: []any{"#bvn"},
expectedErrorContains: "",
},
@@ -177,7 +185,7 @@ func TestFilterExprLogs(t *testing.T) {
category: "Special characters",
query: "question?mark",
shouldPass: true,
expectedQuery: "WHERE match(body, ?)",
expectedQuery: "WHERE match(LOWER(body), LOWER(?))",
expectedArgs: []any{"question?mark"},
expectedErrorContains: "",
},
@@ -185,7 +193,7 @@ func TestFilterExprLogs(t *testing.T) {
category: "Special characters",
query: "backslash\\\\escape",
shouldPass: true,
expectedQuery: "WHERE match(body, ?)",
expectedQuery: "WHERE match(LOWER(body), LOWER(?))",
expectedArgs: []any{"backslash\\\\escape"},
expectedErrorContains: "",
},
@@ -193,7 +201,7 @@ func TestFilterExprLogs(t *testing.T) {
category: "Special characters",
query: "underscore_separator",
shouldPass: true,
expectedQuery: "WHERE match(body, ?)",
expectedQuery: "WHERE match(LOWER(body), LOWER(?))",
expectedArgs: []any{"underscore_separator"},
expectedErrorContains: "",
},
@@ -201,7 +209,7 @@ func TestFilterExprLogs(t *testing.T) {
category: "Special characters",
query: "\"Text with [brackets]\"",
shouldPass: true,
expectedQuery: "WHERE match(body, ?)",
expectedQuery: "WHERE match(LOWER(body), LOWER(?))",
expectedArgs: []any{"Text with [brackets]"},
expectedErrorContains: "",
},
@@ -211,7 +219,7 @@ func TestFilterExprLogs(t *testing.T) {
category: "Multi word",
query: "Fail to parse",
shouldPass: true,
expectedQuery: "WHERE (match(body, ?) AND match(body, ?) AND match(body, ?))",
expectedQuery: "WHERE (match(LOWER(body), LOWER(?)) AND match(LOWER(body), LOWER(?)) AND match(LOWER(body), LOWER(?)))",
expectedArgs: []any{"Fail", "to", "parse"},
expectedErrorContains: "",
},
@@ -219,7 +227,7 @@ func TestFilterExprLogs(t *testing.T) {
category: "Multi word",
query: "Importing file",
shouldPass: true,
expectedQuery: "WHERE (match(body, ?) AND match(body, ?))",
expectedQuery: "WHERE (match(LOWER(body), LOWER(?)) AND match(LOWER(body), LOWER(?)))",
expectedArgs: []any{"Importing", "file"},
expectedErrorContains: "",
},
@@ -227,7 +235,7 @@ func TestFilterExprLogs(t *testing.T) {
category: "Multi word",
query: "sync account status",
shouldPass: true,
expectedQuery: "WHERE (match(body, ?) AND match(body, ?) AND match(body, ?))",
expectedQuery: "WHERE (match(LOWER(body), LOWER(?)) AND match(LOWER(body), LOWER(?)) AND match(LOWER(body), LOWER(?)))",
expectedArgs: []any{"sync", "account", "status"},
expectedErrorContains: "",
},
@@ -235,7 +243,7 @@ func TestFilterExprLogs(t *testing.T) {
category: "Multi word",
query: "Download CSV Reports",
shouldPass: true,
expectedQuery: "WHERE (match(body, ?) AND match(body, ?) AND match(body, ?))",
expectedQuery: "WHERE (match(LOWER(body), LOWER(?)) AND match(LOWER(body), LOWER(?)) AND match(LOWER(body), LOWER(?)))",
expectedArgs: []any{"Download", "CSV", "Reports"},
expectedErrorContains: "",
},
@@ -243,7 +251,7 @@ func TestFilterExprLogs(t *testing.T) {
category: "Multi word",
query: "Emitted event to the Kafka topic",
shouldPass: true,
expectedQuery: "WHERE (match(body, ?) AND match(body, ?) AND match(body, ?) AND match(body, ?) AND match(body, ?) AND match(body, ?))",
expectedQuery: "WHERE (match(LOWER(body), LOWER(?)) AND match(LOWER(body), LOWER(?)) AND match(LOWER(body), LOWER(?)) AND match(LOWER(body), LOWER(?)) AND match(LOWER(body), LOWER(?)) AND match(LOWER(body), LOWER(?)))",
expectedArgs: []any{"Emitted", "event", "to", "the", "Kafka", "topic"},
expectedErrorContains: "",
},
@@ -251,7 +259,7 @@ func TestFilterExprLogs(t *testing.T) {
category: "Multi word",
query: "\"user authentication\" failed",
shouldPass: true,
expectedQuery: "WHERE (match(body, ?) AND match(body, ?))",
expectedQuery: "WHERE (match(LOWER(body), LOWER(?)) AND match(LOWER(body), LOWER(?)))",
expectedArgs: []any{"user authentication", "failed"},
expectedErrorContains: "",
},
@@ -261,7 +269,7 @@ func TestFilterExprLogs(t *testing.T) {
category: "ID search",
query: "250430165501118HIgesxlEb9",
shouldPass: true,
expectedQuery: "WHERE match(body, ?)",
expectedQuery: "WHERE match(LOWER(body), LOWER(?))",
expectedArgs: []any{"250430165501118HIgesxlEb9"},
expectedErrorContains: "",
},
@@ -269,7 +277,7 @@ func TestFilterExprLogs(t *testing.T) {
category: "ID search",
query: "d7b9d77aefa95aef19719775c10fda60c28342f23657d1e27304d6c59a3c3004",
shouldPass: true,
expectedQuery: "WHERE match(body, ?)",
expectedQuery: "WHERE match(LOWER(body), LOWER(?))",
expectedArgs: []any{"d7b9d77aefa95aef19719775c10fda60c28342f23657d1e27304d6c59a3c3004"},
expectedErrorContains: "",
},
@@ -277,7 +285,7 @@ func TestFilterExprLogs(t *testing.T) {
category: "ID search",
query: "51183870",
shouldPass: true,
expectedQuery: "WHERE match(body, ?)",
expectedQuery: "WHERE match(LOWER(body), LOWER(?))",
expectedArgs: []any{"51183870"},
expectedErrorContains: "",
},
@@ -285,7 +293,7 @@ func TestFilterExprLogs(t *testing.T) {
category: "ID search",
query: "79f82635-d014-4f99-adf5-41d31d291ae3",
shouldPass: true,
expectedQuery: "WHERE match(body, ?)",
expectedQuery: "WHERE match(LOWER(body), LOWER(?))",
expectedArgs: []any{"79f82635-d014-4f99-adf5-41d31d291ae3"},
expectedErrorContains: "",
},
@@ -295,7 +303,7 @@ func TestFilterExprLogs(t *testing.T) {
category: "Unicode",
query: "café",
shouldPass: true,
expectedQuery: "WHERE match(body, ?)",
expectedQuery: "WHERE match(LOWER(body), LOWER(?))",
expectedArgs: []any{"café"},
expectedErrorContains: "",
},
@@ -303,7 +311,7 @@ func TestFilterExprLogs(t *testing.T) {
category: "Unicode",
query: "résumé",
shouldPass: true,
expectedQuery: "WHERE match(body, ?)",
expectedQuery: "WHERE match(LOWER(body), LOWER(?))",
expectedArgs: []any{"résumé"},
expectedErrorContains: "",
},
@@ -311,7 +319,7 @@ func TestFilterExprLogs(t *testing.T) {
category: "Unicode",
query: "Россия",
shouldPass: true,
expectedQuery: "WHERE match(body, ?)",
expectedQuery: "WHERE match(LOWER(body), LOWER(?))",
expectedArgs: []any{"Россия"},
expectedErrorContains: "",
},
@@ -319,7 +327,7 @@ func TestFilterExprLogs(t *testing.T) {
category: "Unicode",
query: "\"I do not like emojis ❤️\"",
shouldPass: true,
expectedQuery: "WHERE match(body, ?)",
expectedQuery: "WHERE match(LOWER(body), LOWER(?))",
expectedArgs: []any{"I do not like emojis ❤️"},
expectedErrorContains: "",
},
@@ -329,7 +337,7 @@ func TestFilterExprLogs(t *testing.T) {
category: "Number format",
query: "123",
shouldPass: true,
expectedQuery: "WHERE match(body, ?)",
expectedQuery: "WHERE match(LOWER(body), LOWER(?))",
expectedArgs: []any{"123"},
expectedErrorContains: "",
},
@@ -337,7 +345,7 @@ func TestFilterExprLogs(t *testing.T) {
category: "Number format",
query: "3.14159",
shouldPass: true,
expectedQuery: "WHERE match(body, ?)",
expectedQuery: "WHERE match(LOWER(body), LOWER(?))",
expectedArgs: []any{"3.14159"},
expectedErrorContains: "",
},
@@ -345,7 +353,7 @@ func TestFilterExprLogs(t *testing.T) {
category: "Number format",
query: "-42",
shouldPass: true,
expectedQuery: "WHERE match(body, ?)",
expectedQuery: "WHERE match(LOWER(body), LOWER(?))",
expectedArgs: []any{"-42"},
expectedErrorContains: "",
},
@@ -353,7 +361,7 @@ func TestFilterExprLogs(t *testing.T) {
category: "Number format",
query: "1e6",
shouldPass: true,
expectedQuery: "WHERE match(body, ?)",
expectedQuery: "WHERE match(LOWER(body), LOWER(?))",
expectedArgs: []any{"1e6"},
expectedErrorContains: "",
},
@@ -361,15 +369,15 @@ func TestFilterExprLogs(t *testing.T) {
category: "Number format",
query: "+100",
shouldPass: true,
expectedQuery: "WHERE match(body, ?)",
expectedArgs: []any{"+100"},
expectedQuery: "WHERE match(LOWER(body), LOWER(?))",
expectedArgs: []any{"\\+100"},
expectedErrorContains: "",
},
{
category: "Number format",
query: "0xFF",
shouldPass: true,
expectedQuery: "WHERE match(body, ?)",
expectedQuery: "WHERE match(LOWER(body), LOWER(?))",
expectedArgs: []any{"0xFF"},
expectedErrorContains: "",
},
@@ -379,7 +387,7 @@ func TestFilterExprLogs(t *testing.T) {
category: "FREETEXT with conditions",
query: "critical NOT resolved status=open",
shouldPass: true,
expectedQuery: "WHERE (match(body, ?) AND NOT (match(body, ?)) AND (toString(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?))",
expectedQuery: "WHERE (match(LOWER(body), LOWER(?)) AND NOT (match(LOWER(body), LOWER(?))) AND (toString(attributes_number['status']) = ? AND mapContains(attributes_number, 'status') = ?))",
expectedArgs: []any{"critical", "resolved", "open", true},
expectedErrorContains: "",
},
@@ -387,7 +395,7 @@ func TestFilterExprLogs(t *testing.T) {
category: "FREETEXT with conditions",
query: "database error type=mysql",
shouldPass: true,
expectedQuery: "WHERE (match(body, ?) AND match(body, ?) AND (attributes_string['type'] = ? AND mapContains(attributes_string, 'type') = ?))",
expectedQuery: "WHERE (match(LOWER(body), LOWER(?)) AND match(LOWER(body), LOWER(?)) AND (attributes_string['type'] = ? AND mapContains(attributes_string, 'type') = ?))",
expectedArgs: []any{"database", "error", "mysql", true},
expectedErrorContains: "",
},
@@ -395,7 +403,7 @@ func TestFilterExprLogs(t *testing.T) {
category: "FREETEXT with conditions",
query: "\"connection timeout\" duration>30",
shouldPass: true,
expectedQuery: "WHERE (match(body, ?) AND (toFloat64(attributes_number['duration']) > ? AND mapContains(attributes_number, 'duration') = ?))",
expectedQuery: "WHERE (match(LOWER(body), LOWER(?)) AND (toFloat64(attributes_number['duration']) > ? AND mapContains(attributes_number, 'duration') = ?))",
expectedArgs: []any{"connection timeout", float64(30), true},
expectedErrorContains: "",
},
@@ -403,7 +411,7 @@ func TestFilterExprLogs(t *testing.T) {
category: "FREETEXT with conditions",
query: "warning level=critical",
shouldPass: true,
expectedQuery: "WHERE (match(body, ?) AND (attributes_string['level'] = ? AND mapContains(attributes_string, 'level') = ?))",
expectedQuery: "WHERE (match(LOWER(body), LOWER(?)) AND (attributes_string['level'] = ? AND mapContains(attributes_string, 'level') = ?))",
expectedArgs: []any{"warning", "critical", true},
expectedErrorContains: "",
},
@@ -411,7 +419,7 @@ func TestFilterExprLogs(t *testing.T) {
category: "FREETEXT with conditions",
query: "error service.name=authentication",
shouldPass: true,
expectedQuery: "WHERE (match(body, ?) AND (resources_string['service.name'] = ? AND mapContains(resources_string, 'service.name') = ?))",
expectedQuery: "WHERE (match(LOWER(body), LOWER(?)) AND (resources_string['service.name'] = ? AND mapContains(resources_string, 'service.name') = ?))",
expectedArgs: []any{"error", "authentication", true},
expectedErrorContains: "",
},
@@ -421,7 +429,7 @@ func TestFilterExprLogs(t *testing.T) {
category: "FREETEXT with parentheses",
query: "error (status.code=500 OR status.code=503)",
shouldPass: true,
expectedQuery: "WHERE (match(body, ?) AND (((toFloat64(attributes_number['status.code']) = ? AND mapContains(attributes_number, 'status.code') = ?) OR (toFloat64(attributes_number['status.code']) = ? AND mapContains(attributes_number, 'status.code') = ?))))",
expectedQuery: "WHERE (match(LOWER(body), LOWER(?)) AND (((toFloat64(attributes_number['status.code']) = ? AND mapContains(attributes_number, 'status.code') = ?) OR (toFloat64(attributes_number['status.code']) = ? AND mapContains(attributes_number, 'status.code') = ?))))",
expectedArgs: []any{"error", float64(500), true, float64(503), true},
expectedErrorContains: "",
},
@@ -429,7 +437,7 @@ func TestFilterExprLogs(t *testing.T) {
category: "FREETEXT with parentheses",
query: "(status.code=500 OR status.code=503) error",
shouldPass: true,
expectedQuery: "WHERE ((((toFloat64(attributes_number['status.code']) = ? AND mapContains(attributes_number, 'status.code') = ?) OR (toFloat64(attributes_number['status.code']) = ? AND mapContains(attributes_number, 'status.code') = ?))) AND match(body, ?))",
expectedQuery: "WHERE ((((toFloat64(attributes_number['status.code']) = ? AND mapContains(attributes_number, 'status.code') = ?) OR (toFloat64(attributes_number['status.code']) = ? AND mapContains(attributes_number, 'status.code') = ?))) AND match(LOWER(body), LOWER(?)))",
expectedArgs: []any{float64(500), true, float64(503), true, "error"},
expectedErrorContains: "",
},
@@ -437,7 +445,7 @@ func TestFilterExprLogs(t *testing.T) {
category: "FREETEXT with parentheses",
query: "error AND (status.code=500 OR status.code=503)",
shouldPass: true,
expectedQuery: "WHERE (match(body, ?) AND (((toFloat64(attributes_number['status.code']) = ? AND mapContains(attributes_number, 'status.code') = ?) OR (toFloat64(attributes_number['status.code']) = ? AND mapContains(attributes_number, 'status.code') = ?))))",
expectedQuery: "WHERE (match(LOWER(body), LOWER(?)) AND (((toFloat64(attributes_number['status.code']) = ? AND mapContains(attributes_number, 'status.code') = ?) OR (toFloat64(attributes_number['status.code']) = ? AND mapContains(attributes_number, 'status.code') = ?))))",
expectedArgs: []any{"error", float64(500), true, float64(503), true},
expectedErrorContains: "",
},
@@ -445,7 +453,7 @@ func TestFilterExprLogs(t *testing.T) {
category: "FREETEXT with parentheses",
query: "(status.code=500 OR status.code=503) AND error",
shouldPass: true,
expectedQuery: "WHERE ((((toFloat64(attributes_number['status.code']) = ? AND mapContains(attributes_number, 'status.code') = ?) OR (toFloat64(attributes_number['status.code']) = ? AND mapContains(attributes_number, 'status.code') = ?))) AND match(body, ?))",
expectedQuery: "WHERE ((((toFloat64(attributes_number['status.code']) = ? AND mapContains(attributes_number, 'status.code') = ?) OR (toFloat64(attributes_number['status.code']) = ? AND mapContains(attributes_number, 'status.code') = ?))) AND match(LOWER(body), LOWER(?)))",
expectedArgs: []any{float64(500), true, float64(503), true, "error"},
expectedErrorContains: "",
},
@@ -455,7 +463,7 @@ func TestFilterExprLogs(t *testing.T) {
category: "Whitespace with FREETEXT",
query: "term1 term2",
shouldPass: true,
expectedQuery: "WHERE (match(body, ?) AND match(body, ?))",
expectedQuery: "WHERE (match(LOWER(body), LOWER(?)) AND match(LOWER(body), LOWER(?)))",
expectedArgs: []any{"term1", "term2"},
expectedErrorContains: "",
},
@@ -465,7 +473,7 @@ func TestFilterExprLogs(t *testing.T) {
category: "Key token conflict",
query: "status.code",
shouldPass: true,
expectedQuery: "WHERE match(body, ?)",
expectedQuery: "WHERE match(LOWER(body), LOWER(?))",
expectedArgs: []any{"status.code"},
expectedErrorContains: "",
},
@@ -473,7 +481,7 @@ func TestFilterExprLogs(t *testing.T) {
category: "Key token conflict",
query: "array_field",
shouldPass: true,
expectedQuery: "WHERE match(body, ?)",
expectedQuery: "WHERE match(LOWER(body), LOWER(?))",
expectedArgs: []any{"array_field"},
expectedErrorContains: "",
},
@@ -481,7 +489,7 @@ func TestFilterExprLogs(t *testing.T) {
category: "Key token conflict",
query: "user_id.value",
shouldPass: true,
expectedQuery: "WHERE match(body, ?)",
expectedQuery: "WHERE match(LOWER(body), LOWER(?))",
expectedArgs: []any{"user_id.value"},
expectedErrorContains: "",
}, // Could be a key with dot notation or FREETEXT
@@ -491,7 +499,7 @@ func TestFilterExprLogs(t *testing.T) {
category: "Random cases",
query: "true",
shouldPass: true,
expectedQuery: "WHERE match(body, ?)",
expectedQuery: "WHERE match(LOWER(body), LOWER(?))",
expectedArgs: []any{"true"},
expectedErrorContains: "",
}, // Could be interpreted as boolean or FREETEXT
@@ -499,7 +507,7 @@ func TestFilterExprLogs(t *testing.T) {
category: "Random cases",
query: "false",
shouldPass: true,
expectedQuery: "WHERE match(body, ?)",
expectedQuery: "WHERE match(LOWER(body), LOWER(?))",
expectedArgs: []any{"false"},
expectedErrorContains: "",
}, // Could be interpreted as boolean or FREETEXT
@@ -507,7 +515,7 @@ func TestFilterExprLogs(t *testing.T) {
category: "Random cases",
query: "null",
shouldPass: true,
expectedQuery: "WHERE match(body, ?)",
expectedQuery: "WHERE match(LOWER(body), LOWER(?))",
expectedArgs: []any{"null"},
expectedErrorContains: "",
}, // Special value or FREETEXT
@@ -515,7 +523,7 @@ func TestFilterExprLogs(t *testing.T) {
category: "Random cases",
query: "123abc",
shouldPass: true,
expectedQuery: "WHERE match(body, ?)",
expectedQuery: "WHERE match(LOWER(body), LOWER(?))",
expectedArgs: []any{"123abc"},
expectedErrorContains: "",
}, // Starts with number but contains letters
@@ -523,7 +531,7 @@ func TestFilterExprLogs(t *testing.T) {
category: "Random cases",
query: "0x123F",
shouldPass: true,
expectedQuery: "WHERE match(body, ?)",
expectedQuery: "WHERE match(LOWER(body), LOWER(?))",
expectedArgs: []any{"0x123F"},
expectedErrorContains: "",
}, // Hex number format
@@ -531,7 +539,7 @@ func TestFilterExprLogs(t *testing.T) {
category: "Random cases",
query: "1.2.3",
shouldPass: true,
expectedQuery: "WHERE match(body, ?)",
expectedQuery: "WHERE match(LOWER(body), LOWER(?))",
expectedArgs: []any{"1.2.3"},
expectedErrorContains: "",
}, // Version number format
@@ -539,7 +547,7 @@ func TestFilterExprLogs(t *testing.T) {
category: "Random cases",
query: "a+b-c*d/e",
shouldPass: true,
expectedQuery: "WHERE match(body, ?)",
expectedQuery: "WHERE match(LOWER(body), LOWER(?))",
expectedArgs: []any{"a+b-c*d/e"},
expectedErrorContains: "",
}, // Mathematical expression as FREETEXT
@@ -547,7 +555,7 @@ func TestFilterExprLogs(t *testing.T) {
category: "Random cases",
query: "http://example.com/path",
shouldPass: true,
expectedQuery: "WHERE match(body, ?)",
expectedQuery: "WHERE match(LOWER(body), LOWER(?))",
expectedArgs: []any{"http://example.com/path"},
expectedErrorContains: "",
}, // URL as FREETEXT
@@ -655,7 +663,7 @@ func TestFilterExprLogs(t *testing.T) {
category: "Key-operator-value boundary",
query: `"not!equal"`,
shouldPass: true,
expectedQuery: "WHERE match(body, ?)",
expectedQuery: "WHERE match(LOWER(body), LOWER(?))",
expectedArgs: []any{"not!equal"},
expectedErrorContains: "",
},
@@ -671,7 +679,7 @@ func TestFilterExprLogs(t *testing.T) {
category: "Key-operator-value boundary",
query: `"greater>than"`,
shouldPass: true,
expectedQuery: "WHERE match(body, ?)",
expectedQuery: "WHERE match(LOWER(body), LOWER(?))",
expectedArgs: []any{"greater>than"},
expectedErrorContains: "",
},
@@ -687,7 +695,7 @@ func TestFilterExprLogs(t *testing.T) {
category: "Key-operator-value boundary",
query: `"less<than"`,
shouldPass: true,
expectedQuery: "WHERE match(body, ?)",
expectedQuery: "WHERE match(LOWER(body), LOWER(?))",
expectedArgs: []any{"less<than"},
expectedErrorContains: "",
},
@@ -695,7 +703,7 @@ func TestFilterExprLogs(t *testing.T) {
category: "Key-operator-value boundary",
query: "single'quote'",
shouldPass: true,
expectedQuery: "WHERE (match(body, ?) AND match(body, ?))",
expectedQuery: "WHERE (match(LOWER(body), LOWER(?)) AND match(LOWER(body), LOWER(?)))",
expectedArgs: []any{"single", "quote"},
expectedErrorContains: "",
},
@@ -703,7 +711,7 @@ func TestFilterExprLogs(t *testing.T) {
category: "Key-operator-value boundary",
query: "quoted\"text\"",
shouldPass: true,
expectedQuery: "WHERE (match(body, ?) AND match(body, ?))",
expectedQuery: "WHERE (match(LOWER(body), LOWER(?)) AND match(LOWER(body), LOWER(?)))",
expectedArgs: []any{"quoted", "text"},
expectedErrorContains: "",
},
@@ -719,7 +727,7 @@ func TestFilterExprLogs(t *testing.T) {
category: "Key-operator-value boundary",
query: "function(param)",
shouldPass: true,
expectedQuery: "WHERE (match(body, ?) AND (match(body, ?)))",
expectedQuery: "WHERE (match(LOWER(body), LOWER(?)) AND (match(LOWER(body), LOWER(?))))",
expectedArgs: []any{"function", "param"},
expectedErrorContains: "",
},
@@ -727,7 +735,7 @@ func TestFilterExprLogs(t *testing.T) {
category: "Key-operator-value boundary",
query: `"function(param)"`,
shouldPass: true,
expectedQuery: "WHERE match(body, ?)",
expectedQuery: "WHERE match(LOWER(body), LOWER(?))",
expectedArgs: []any{"function(param)"},
expectedErrorContains: "",
},
@@ -743,7 +751,7 @@ func TestFilterExprLogs(t *testing.T) {
category: "Key-operator-value boundary",
query: `"user=admin"`,
shouldPass: true,
expectedQuery: "WHERE match(body, ?)",
expectedQuery: "WHERE match(LOWER(body), LOWER(?))",
expectedArgs: []any{"user=admin"},
expectedErrorContains: "",
},
@@ -1357,7 +1365,7 @@ func TestFilterExprLogs(t *testing.T) {
category: "REGEXP operator",
query: `"^\[(INFO|WARN|ERROR|DEBUG)\] .+$"`,
shouldPass: true,
expectedQuery: "WHERE match(body, ?)",
expectedQuery: "WHERE match(LOWER(body), LOWER(?))",
expectedArgs: []any{`^\[(INFO|WARN|ERROR|DEBUG)\] .+$`},
expectedErrorContains: "",
},

View File

@@ -266,7 +266,7 @@ func TestStatementBuilderListQueryResourceTests(t *testing.T) {
Limit: 10,
},
expected: qbtypes.Statement{
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND match(body, ?) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? LIMIT ?",
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND match(LOWER(body), LOWER(?)) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? LIMIT ?",
Args: []any{uint64(1747945619), uint64(1747983448), "hello", "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10},
},
expectedErr: nil,
@@ -294,7 +294,7 @@ func TestStatementBuilderListQueryResourceTests(t *testing.T) {
},
},
expected: qbtypes.Statement{
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE ((simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?)) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND (match(body, ?)) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? ORDER BY `attribute_string_materialized$$key$$name` AS `materialized.key.name` desc LIMIT ?",
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE ((simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?)) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND (match(LOWER(body), LOWER(?))) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? ORDER BY `attribute_string_materialized$$key$$name` AS `materialized.key.name` desc LIMIT ?",
Args: []any{"cartservice", "%service.name%", "%service.name\":\"cartservice%", uint64(1747945619), uint64(1747983448), "hello", "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10},
},
expectedErr: nil,

View File

@@ -63,11 +63,11 @@ func (v *TelemetryFieldVisitor) VisitColumnDef(expr *parser.ColumnDef) error {
case "bool":
fieldDataType = telemetrytypes.FieldDataTypeBool
case "int", "int64":
fieldDataType = telemetrytypes.FieldDataTypeFloat64
fieldDataType = telemetrytypes.FieldDataTypeNumber
case "float", "float64":
fieldDataType = telemetrytypes.FieldDataTypeFloat64
fieldDataType = telemetrytypes.FieldDataTypeNumber
case "number":
fieldDataType = telemetrytypes.FieldDataTypeFloat64
fieldDataType = telemetrytypes.FieldDataTypeNumber
default:
return nil // Unknown data type
}

View File

@@ -121,7 +121,7 @@ func TestExtractFieldKeysFromTblStatement(t *testing.T) {
{
Name: "input_size",
FieldContext: telemetrytypes.FieldContextAttribute,
FieldDataType: telemetrytypes.FieldDataTypeFloat64,
FieldDataType: telemetrytypes.FieldDataTypeNumber,
Materialized: true,
},
{

View File

@@ -1,959 +0,0 @@
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
}

View File

@@ -1,89 +0,0 @@
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)
}

View File

@@ -73,7 +73,7 @@ func (m *alertMigrateV5) Migrate(ctx context.Context, ruleData map[string]any) b
panelType = pt
}
if m.updateQueryData(ctx, queryMap, "v4", panelType) {
if m.updateQueryData(ctx, queryMap, version, panelType) {
updated = true
}
m.logger.InfoContext(ctx, "migrated querymap")

View File

@@ -366,7 +366,7 @@ func (mc *migrateCommon) createAggregations(ctx context.Context, queryData map[s
aggregation = map[string]any{
"metricName": aggregateAttr["key"],
"temporality": queryData["temporality"],
"timeAggregation": aggregateOp,
"timeAggregation": queryData["timeAggregation"],
"spaceAggregation": queryData["spaceAggregation"],
}
if reduceTo, ok := queryData["reduceTo"].(string); ok {

View File

@@ -52,8 +52,3 @@ type StatementBuilder[T any] interface {
// Build builds the query.
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)
}

View File

@@ -88,8 +88,8 @@ func (q *QueryEnvelope) UnmarshalJSON(data []byte) error {
case QueryTypeTraceOperator:
var spec QueryBuilderTraceOperator
if err := UnmarshalJSONWithContext(shadow.Spec, &spec, "trace operator spec"); err != nil {
return wrapUnmarshalError(err, "invalid trace operator spec: %v", err)
if err := json.Unmarshal(shadow.Spec, &spec); err != nil {
return errors.WrapInvalidInputf(err, errors.CodeInvalidInput, "invalid trace operator spec")
}
q.Spec = spec
@@ -113,7 +113,7 @@ func (q *QueryEnvelope) UnmarshalJSON(data []byte) error {
"unknown query type %q",
shadow.Type,
).WithAdditional(
"Valid query types are: builder_query, builder_sub_query, builder_formula, builder_join, builder_trace_operator, promql, clickhouse_sql",
"Valid query types are: builder_query, builder_sub_query, builder_formula, builder_join, promql, clickhouse_sql",
)
}

View File

@@ -131,7 +131,7 @@ func TestQueryRangeRequest_UnmarshalJSON(t *testing.T) {
"filter": {
"expression": "trace_duration > 200ms AND span_count >= 5"
},
"order": [{
"orderBy": [{
"key": {
"name": "trace_duration"
},
@@ -230,7 +230,7 @@ func TestQueryRangeRequest_UnmarshalJSON(t *testing.T) {
"name": "complex_trace_analysis",
"expression": "A => (B && NOT C)",
"filter": { "expression": "trace_duration BETWEEN 100ms AND 5s AND span_count IN (5, 10, 15)" },
"order": [{
"orderBy": [{
"key": { "name": "span_count" },
"direction": "asc"
}],
@@ -1028,17 +1028,15 @@ func TestQueryRangeRequest_UnmarshalJSON(t *testing.T) {
func TestParseTraceExpression(t *testing.T) {
tests := []struct {
name string
expression string
expectError bool
expectedOpCount int
checkResult func(t *testing.T, result *TraceOperand)
name string
expression string
expectError bool
checkResult func(t *testing.T, result *TraceOperand)
}{
{
name: "simple query reference",
expression: "A",
expectError: false,
expectedOpCount: 0,
name: "simple query reference",
expression: "A",
expectError: false,
checkResult: func(t *testing.T, result *TraceOperand) {
assert.NotNil(t, result.QueryRef)
assert.Equal(t, "A", result.QueryRef.Name)
@@ -1046,10 +1044,9 @@ func TestParseTraceExpression(t *testing.T) {
},
},
{
name: "simple implication",
expression: "A => B",
expectError: false,
expectedOpCount: 1,
name: "simple implication",
expression: "A => B",
expectError: false,
checkResult: func(t *testing.T, result *TraceOperand) {
assert.NotNil(t, result.Operator)
assert.Equal(t, TraceOperatorDirectDescendant, *result.Operator)
@@ -1060,10 +1057,9 @@ func TestParseTraceExpression(t *testing.T) {
},
},
{
name: "and operation",
expression: "A && B",
expectError: false,
expectedOpCount: 1,
name: "and operation",
expression: "A && B",
expectError: false,
checkResult: func(t *testing.T, result *TraceOperand) {
assert.NotNil(t, result.Operator)
assert.Equal(t, TraceOperatorAnd, *result.Operator)
@@ -1072,10 +1068,9 @@ func TestParseTraceExpression(t *testing.T) {
},
},
{
name: "or operation",
expression: "A || B",
expectError: false,
expectedOpCount: 1,
name: "or operation",
expression: "A || B",
expectError: false,
checkResult: func(t *testing.T, result *TraceOperand) {
assert.NotNil(t, result.Operator)
assert.Equal(t, TraceOperatorOr, *result.Operator)
@@ -1084,10 +1079,9 @@ func TestParseTraceExpression(t *testing.T) {
},
},
{
name: "unary NOT operation",
expression: "NOT A",
expectError: false,
expectedOpCount: 1,
name: "unary NOT operation",
expression: "NOT A",
expectError: false,
checkResult: func(t *testing.T, result *TraceOperand) {
assert.NotNil(t, result.Operator)
assert.Equal(t, TraceOperatorNot, *result.Operator)
@@ -1097,10 +1091,9 @@ func TestParseTraceExpression(t *testing.T) {
},
},
{
name: "binary NOT operation",
expression: "A NOT B",
expectError: false,
expectedOpCount: 1,
name: "binary NOT operation",
expression: "A NOT B",
expectError: false,
checkResult: func(t *testing.T, result *TraceOperand) {
assert.NotNil(t, result.Operator)
assert.Equal(t, TraceOperatorExclude, *result.Operator)
@@ -1111,10 +1104,9 @@ func TestParseTraceExpression(t *testing.T) {
},
},
{
name: "complex expression with precedence",
expression: "A => B && C || D",
expectError: false,
expectedOpCount: 3, // Three operators: =>, &&, ||
name: "complex expression with precedence",
expression: "A => B && C || D",
expectError: false,
checkResult: func(t *testing.T, result *TraceOperand) {
// Should parse as: A => (B && (C || D)) due to precedence: NOT > || > && > =>
// The parsing finds operators from lowest precedence first
@@ -1128,10 +1120,9 @@ func TestParseTraceExpression(t *testing.T) {
},
},
{
name: "simple parentheses",
expression: "(A)",
expectError: false,
expectedOpCount: 0,
name: "simple parentheses",
expression: "(A)",
expectError: false,
checkResult: func(t *testing.T, result *TraceOperand) {
assert.NotNil(t, result.QueryRef)
assert.Equal(t, "A", result.QueryRef.Name)
@@ -1139,10 +1130,9 @@ func TestParseTraceExpression(t *testing.T) {
},
},
{
name: "parentheses expression",
expression: "A => (B || C)",
expectError: false,
expectedOpCount: 2, // Two operators: =>, ||
name: "parentheses expression",
expression: "A => (B || C)",
expectError: false,
checkResult: func(t *testing.T, result *TraceOperand) {
assert.NotNil(t, result.Operator)
assert.Equal(t, TraceOperatorDirectDescendant, *result.Operator)
@@ -1156,10 +1146,9 @@ func TestParseTraceExpression(t *testing.T) {
},
},
{
name: "nested NOT with parentheses",
expression: "NOT (A && B)",
expectError: false,
expectedOpCount: 2, // Two operators: NOT, &&
name: "nested NOT with parentheses",
expression: "NOT (A && B)",
expectError: false,
checkResult: func(t *testing.T, result *TraceOperand) {
assert.NotNil(t, result.Operator)
assert.Equal(t, TraceOperatorNot, *result.Operator)
@@ -1170,13 +1159,6 @@ func TestParseTraceExpression(t *testing.T) {
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",
expression: "123",
@@ -1192,11 +1174,11 @@ func TestParseTraceExpression(t *testing.T) {
expression: "",
expectError: true,
},
{
name: "expression with extra whitespace",
expression: " A => B ",
expectError: false,
expectedOpCount: 1,
name: "expression with extra whitespace",
expression: " A => B ",
expectError: false,
checkResult: func(t *testing.T, result *TraceOperand) {
assert.NotNil(t, result.Operator)
assert.Equal(t, TraceOperatorDirectDescendant, *result.Operator)
@@ -1208,7 +1190,7 @@ func TestParseTraceExpression(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, opCount, err := parseTraceExpression(tt.expression)
result, err := parseTraceExpression(tt.expression)
if tt.expectError {
assert.Error(t, err)
@@ -1218,8 +1200,6 @@ func TestParseTraceExpression(t *testing.T) {
require.NoError(t, err)
require.NotNil(t, result)
assert.Equal(t, tt.expectedOpCount, opCount, "operator count mismatch")
if tt.checkResult != nil {
tt.checkResult(t, result)
}
@@ -1227,63 +1207,6 @@ 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) {
tests := []struct {
name string

View File

@@ -29,11 +29,6 @@ var (
OrderByTraceDuration = TraceOrderBy{valuer.NewString("trace_duration")}
)
const (
// MaxTraceOperators defines the maximum number of operators allowed in a trace expression
MaxTraceOperators = 10
)
type QueryBuilderTraceOperator struct {
Name string `json:"name"`
Disabled bool `json:"disabled,omitempty"`
@@ -46,21 +41,15 @@ type QueryBuilderTraceOperator struct {
ReturnSpansFrom string `json:"returnSpansFrom,omitempty"`
// Trace-specific ordering (only span_count and trace_duration allowed)
Order []OrderBy `json:"order,omitempty"`
Order []OrderBy `json:"orderBy,omitempty"`
Aggregations []TraceAggregation `json:"aggregations,omitempty"`
StepInterval Step `json:"stepInterval,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"`
Offset int `json:"offset,omitempty"`
Cursor string `json:"cursor,omitempty"`
Legend string `json:"legend,omitempty"`
// Other post-processing options
SelectFields []telemetrytypes.TelemetryFieldKey `json:"selectFields,omitempty"`
Functions []Function `json:"functions,omitempty"`
@@ -95,7 +84,7 @@ func (q *QueryBuilderTraceOperator) ParseExpression() error {
)
}
parsed, operatorCount, err := parseTraceExpression(q.Expression)
parsed, err := parseTraceExpression(q.Expression)
if err != nil {
return errors.WrapInvalidInputf(
err,
@@ -105,24 +94,13 @@ 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
return nil
}
// ValidateTraceOperator validates that all referenced queries exist and are trace queries
func (q *QueryBuilderTraceOperator) ValidateTraceOperator(queries []QueryEnvelope) error {
// Parse the expression - this now includes operator count validation
// Parse the expression
if err := q.ParseExpression(); err != nil {
return err
}
@@ -179,15 +157,6 @@ 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
if q.ReturnSpansFrom != "" {
if _, exists := availableQueries[q.ReturnSpansFrom]; !exists {
@@ -265,15 +234,6 @@ 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
if q.Limit > 10000 {
return errors.WrapInvalidInputf(
@@ -316,11 +276,6 @@ func (q *QueryBuilderTraceOperator) collectReferencedQueries(operand *TraceOpera
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
func ValidateUniqueTraceOperator(queries []QueryEnvelope) error {
traceOperatorCount := 0
@@ -349,8 +304,9 @@ func ValidateUniqueTraceOperator(queries []QueryEnvelope) error {
return nil
}
// parseTraceExpression parses an expression string into a tree structure
// Handles precedence: NOT (highest) > || > && > => (lowest)
func parseTraceExpression(expr string) (*TraceOperand, int, error) {
func parseTraceExpression(expr string) (*TraceOperand, error) {
expr = strings.TrimSpace(expr)
// Handle parentheses
@@ -363,15 +319,15 @@ func parseTraceExpression(expr string) (*TraceOperand, int, error) {
// Handle unary NOT operator (prefix)
if strings.HasPrefix(expr, "NOT ") {
operand, count, err := parseTraceExpression(expr[4:])
operand, err := parseTraceExpression(expr[4:])
if err != nil {
return nil, 0, err
return nil, err
}
notOp := TraceOperatorNot
return &TraceOperand{
Operator: &notOp,
Left: operand,
}, count + 1, nil // Add 1 for this NOT operator
}, nil
}
// Find binary operators with lowest precedence first (=> has lowest precedence)
@@ -383,14 +339,14 @@ func parseTraceExpression(expr string) (*TraceOperand, int, error) {
leftExpr := strings.TrimSpace(expr[:pos])
rightExpr := strings.TrimSpace(expr[pos+len(op):])
left, leftCount, err := parseTraceExpression(leftExpr)
left, err := parseTraceExpression(leftExpr)
if err != nil {
return nil, 0, err
return nil, err
}
right, rightCount, err := parseTraceExpression(rightExpr)
right, err := parseTraceExpression(rightExpr)
if err != nil {
return nil, 0, err
return nil, err
}
var opType TraceOperatorType
@@ -409,13 +365,13 @@ func parseTraceExpression(expr string) (*TraceOperand, int, error) {
Operator: &opType,
Left: left,
Right: right,
}, leftCount + rightCount + 1, nil // Add counts from both sides + 1 for this operator
}, nil
}
}
// If no operators found, this should be a query reference
if matched, _ := regexp.MatchString(`^[A-Za-z][A-Za-z0-9_]*$`, expr); !matched {
return nil, 0, errors.WrapInvalidInputf(
return nil, errors.WrapInvalidInputf(
nil,
errors.CodeInvalidInput,
"invalid query reference '%s'",
@@ -423,10 +379,9 @@ func parseTraceExpression(expr string) (*TraceOperand, int, error) {
)
}
// Leaf node - no operators
return &TraceOperand{
QueryRef: &TraceOperatorQueryRef{Name: expr},
}, 0, nil
}, nil
}
// isBalancedParentheses checks if parentheses are balanced in the expression

View File

@@ -36,11 +36,6 @@ func getQueryIdentifier(envelope QueryEnvelope, index int) string {
return fmt.Sprintf("formula '%s'", spec.Name)
}
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:
if spec, ok := envelope.Spec.(QueryBuilderJoin); ok && spec.Name != "" {
return fmt.Sprintf("join '%s'", spec.Name)
@@ -569,24 +564,6 @@ func (r *QueryRangeRequest) validateCompositeQuery() error {
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:
// PromQL validation is handled separately
spec, ok := envelope.Spec.(PromQuery)
@@ -633,7 +610,7 @@ func (r *QueryRangeRequest) validateCompositeQuery() error {
envelope.Type,
queryId,
).WithAdditional(
"Valid query types are: builder_query, builder_formula, builder_join, promql, clickhouse_sql, trace_operator",
"Valid query types are: builder_query, builder_formula, builder_join, promql, clickhouse_sql",
)
}
}
@@ -701,21 +678,6 @@ func validateQueryEnvelope(envelope QueryEnvelope, requestType RequestType) erro
)
}
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:
spec, ok := envelope.Spec.(PromQuery)
if !ok {
@@ -752,7 +714,7 @@ func validateQueryEnvelope(envelope QueryEnvelope, requestType RequestType) erro
"unknown query type: %s",
envelope.Type,
).WithAdditional(
"Valid query types are: builder_query, builder_sub_query, builder_formula, builder_join, promql, clickhouse_sql, trace_operator",
"Valid query types are: builder_query, builder_sub_query, builder_formula, builder_join, promql, clickhouse_sql",
)
}
}

View File

@@ -11,6 +11,8 @@ import (
"github.com/SigNoz/signoz/pkg/query-service/model"
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
"github.com/SigNoz/signoz/pkg/query-service/utils/labels"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
)
// this file contains common structs and methods used by
@@ -131,10 +133,30 @@ func (rc *RuleCondition) GetSelectedQueryName() string {
for name := range rc.CompositeQuery.BuilderQueries {
queryNames[name] = struct{}{}
}
for _, query := range rc.CompositeQuery.Queries {
switch spec := query.Spec.(type) {
case qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]:
queryNames[spec.Name] = struct{}{}
case qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]:
queryNames[spec.Name] = struct{}{}
case qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]:
queryNames[spec.Name] = struct{}{}
case qbtypes.QueryBuilderFormula:
queryNames[spec.Name] = struct{}{}
}
}
} else if rc.QueryType() == v3.QueryTypeClickHouseSQL {
for name := range rc.CompositeQuery.ClickHouseQueries {
queryNames[name] = struct{}{}
}
for _, query := range rc.CompositeQuery.Queries {
switch spec := query.Spec.(type) {
case qbtypes.ClickHouseQuery:
queryNames[spec.Name] = struct{}{}
}
}
}
}