Compare commits

..

87 Commits

Author SHA1 Message Date
eKuG
7206bb82fe Merge branch 'trace_operator_implementation' into demo/trace_operators_backend 2025-08-21 13:44:01 +05:30
eKuG
a1ad2b7835 feat: resolved conflicts 2025-08-21 13:43:17 +05:30
eKuG
a2ab97a347 Merge branch 'demo/trace-operators' of github.com:SigNoz/signoz into demo/trace_operators_backend 2025-08-21 10:18:35 +05:30
ahrefabhi
7c1ca7544d Merge branch 'feature/trace-operators' into demo/trace-operators 2025-08-21 10:15:51 +05:30
ahrefabhi
1b0dcb86b5 chore: linter fix 2025-08-21 09:50:35 +05:30
ahrefabhi
cb49bc795b chore: minor pr review change 2025-08-21 01:33:10 +05:30
ahrefabhi
3f1aeb3077 chore: added traceoperators in alerts 2025-08-21 01:31:40 +05:30
ahrefabhi
cc2a905e0b chore: minor changes in queryaddon and aggregation for support 2025-08-21 01:30:37 +05:30
ahrefabhi
eba024fc5d chore: removed traceoperations and reused queryoperations 2025-08-21 01:29:30 +05:30
ahrefabhi
561ec8fd40 chore: added ui changes in the editor 2025-08-21 01:28:32 +05:30
ahrefabhi
aa1dfc6eb1 feat: added span selector 2025-08-21 01:27:58 +05:30
Vibhu Pandey
a3f32b3d85 fix(comment): add a dedicated comment parsing middleware (#8855)
## 📄 Summary

- add a dedicated comment parsing middleware. This removes duplication and double parsing of referrer.
2025-08-20 20:20:28 +05:30
eKuG
3248012716 feat: resolved conflicts 2025-08-20 18:59:21 +05:30
eKuG
4ce56ebab4 feat: resolved conflicts 2025-08-20 18:58:43 +05:30
eKuG
bb80d69819 feat: resolved conflicts 2025-08-20 17:32:15 +05:30
eKuG
49aaecd02c feat: resolved conflicts 2025-08-20 17:30:52 +05:30
eKuG
98f4e840cd feat: resolved conflicts 2025-08-20 17:20:44 +05:30
eKuG
74824e7853 feat: resolved conflicts 2025-08-20 16:59:01 +05:30
ahrefabhi
b574fee2d4 chore: fixed minor styles + minor ux fix 2025-08-20 15:18:11 +05:30
eKuG
675b66a7b9 feat: resolved conflicts 2025-08-20 12:18:37 +05:30
Ekansh Gupta
f55aeb5b5a Merge branch 'main' into trace_operator_implementation 2025-08-20 11:45:46 +05:30
eKuG
ae3806ce64 feat: resolved conflicts 2025-08-20 11:45:04 +05:30
ahrefabhi
9c489ebc84 chore: Added changes to prepare request payload 2025-08-20 11:24:19 +05:30
ahrefabhi
f6d432cfce chore: added initialvalue for trace operators 2025-08-20 11:23:42 +05:30
ahrefabhi
6ca6f615b0 chore: type changes 2025-08-20 11:22:40 +05:30
ahrefabhi
36e7820edd chore: minor UI fixes 2025-08-20 11:21:16 +05:30
ahrefabhi
f51cce844b feat: added conditions for traceoperator 2025-08-20 11:20:51 +05:30
ahrefabhi
b2d3d61b44 chore: minor style improvments 2025-08-20 11:20:06 +05:30
ahrefabhi
4e2c7c6309 feat: added traceoperator component and styles 2025-08-20 11:19:35 +05:30
Amlan Kumar Nandy
9c2f127282 chore: backend changes for y-axis management (#8730) 2025-08-20 04:04:50 +00:00
eKuG
885045d704 feat: resolved conflicts 2025-08-19 13:41:23 +05:30
Ekansh Gupta
9dc2e82ce1 Merge branch 'main' into trace_operator_implementation 2025-08-19 13:10:39 +05:30
Srikanth Chekuri
e30de5f13e chore: do not store query name in cache (#8838) 2025-08-19 13:01:55 +05:30
SagarRajput-7
019083983a fix: added sanity logic for explorer old urls (#8804)
* fix: added sanity logic for explorer old urls

* fix: added test for useSanitizeOrderBy

* fix: added sentry events for orderby validation

* fix: cleanup unused logic and renamed boolean state
2025-08-19 12:46:02 +05:30
eKuG
19e60ee688 feat: resolved conflicts 2025-08-19 12:26:51 +05:30
Shaheer Kochai
fdcad997f5 feat: trace detail page actionables (#8761)
* feat: add table view with actionables to span details drawer attributes

* feat: span actions functionality

* refactor: overall improvements

* feat: revert to key-value pair UI with hover actions

* feat: add support for copying trace attribute value on click

* refactor: prevent prop drilling and access return values from useTraceActions in AttributeActions

* refactor: integrate filter conversion logic into useTraceActions for improved query handling
2025-08-19 06:50:28 +00:00
Yunus M
03359a40a2 fix: set source on add new query (#8836)
Co-authored-by: Vikrant Gupta <vikrant@signoz.io>
2025-08-19 05:51:33 +00:00
eKuG
ea89714cb4 feat: resolved conflicts 2025-08-19 11:20:32 +05:30
Abhi kumar
4f45801729 fix: added fix for supporting older queries (#8834)
* fix: added fix for supporting older queries

* fix: added fix for exist operator

* chore: minor fix for quick filters

* chore: added tests for convertfilterstoexpression

* chore: added fix for regex to regexp conversion

* test: added test for regex to regexp

* fix: added fix for functions conversion and tests

* fix: added fix for negated non_value_operators
2025-08-19 10:55:45 +05:30
eKuG
4be618bcde feat: resolved conflicts 2025-08-18 16:45:47 +05:30
eKuG
2bfecce3cb feat: resolved conflicts 2025-08-18 16:17:48 +05:30
eKuG
eefbcbd1eb feat: resolved conflicts 2025-08-18 15:43:49 +05:30
eKuG
a3f366ee36 feat: resolved conflicts 2025-08-18 15:35:45 +05:30
eKuG
cff547c303 feat: resolved conflicts 2025-08-18 15:28:53 +05:30
Ekansh Gupta
d6287cba52 Merge branch 'main' into trace_operator_implementation 2025-08-18 15:26:31 +05:30
eKuG
44b09fbef2 feat: resolved conflicts 2025-08-18 15:26:08 +05:30
eKuG
081eb64893 feat: resolved conflicts 2025-08-11 13:03:23 +05:30
eKuG
6338af55dd feat: resolved conflicts 2025-08-11 12:44:17 +05:30
eKuG
5450b92650 feat: resolved conflicts 2025-08-11 11:52:33 +05:30
Ekansh Gupta
a9179321e1 Merge branch 'main' into trace_operator_implementation 2025-08-11 11:48:28 +05:30
eKuG
90366975d8 feat: resolved conflicts 2025-08-11 11:48:13 +05:30
eKuG
33f47993d3 feat: resolved conflicts 2025-08-11 11:46:47 +05:30
eKuG
9170846111 feat: resolved conflicts 2025-08-11 11:44:03 +05:30
Ekansh Gupta
54baa9d76d Merge branch 'main' into trace_operator_implementation 2025-07-29 15:43:40 +05:30
eKuG
0ed6aac74e feat: refactored the consume function 2025-07-29 13:09:49 +05:30
Ekansh Gupta
b994fed409 Merge branch 'main' into trace_operator_implementation 2025-07-29 13:08:40 +05:30
eKuG
a9eb992f67 feat: refactored the consume function 2025-07-29 13:08:20 +05:30
eKuG
ed95815a6a feat: refactored the consume function 2025-07-29 13:06:32 +05:30
eKuG
2e2888346f feat: refactored the consume function 2025-07-29 12:24:44 +05:30
eKuG
525c5ac081 feat: refactored the consume function 2025-07-29 12:23:22 +05:30
eKuG
66cede4c03 feat: added postprocess 2025-07-28 23:29:27 +05:30
eKuG
33ea94991a feat: added postprocess 2025-07-28 23:28:10 +05:30
Ekansh Gupta
bae461d1f8 Merge branch 'main' into trace_operator_implementation 2025-07-28 21:24:02 +05:30
eKuG
9df82cc952 feat: added postprocess 2025-07-28 21:19:53 +05:30
Ekansh Gupta
d3d927c84d Merge branch 'main' into trace_operator_implementation 2025-07-28 14:24:46 +05:30
eKuG
36ab1ce8a2 feat: refactor trace operator 2025-07-25 17:55:13 +05:30
Ekansh Gupta
7bbf3ffba3 Merge branch 'main' into trace_operator_implementation 2025-07-25 13:56:43 +05:30
Ekansh Gupta
6ab5c3cf2e Merge branch 'main' into trace_operator_implementation 2025-07-23 15:35:13 +05:30
eKuG
c2384e387d feat: added implementation of trace operators 2025-07-07 21:18:46 +05:30
eKuG
a00f263bad feat: added implementation of trace operators 2025-06-29 13:35:49 +05:30
eKuG
9d648915cc feat: added implementation of trace operators 2025-06-23 16:24:01 +05:30
eKuG
e6bd7484fa feat: added implementation of trace operators 2025-06-23 16:13:02 +05:30
Ekansh Gupta
d780c7482e Merge branch 'main' into trace_operator_implementation 2025-06-23 16:00:33 +05:30
eKuG
ffa8d0267e feat: added implementation of trace operators 2025-06-23 15:59:53 +05:30
Ekansh Gupta
f0505a9c0e Merge branch 'main' into trace_operator_implementation 2025-06-22 15:44:55 +05:30
eKuG
09e212bd64 feat: added implementation of trace operators 2025-06-22 15:43:33 +05:30
eKuG
75f3131e65 feat: added implementation of trace operators 2025-06-22 15:39:43 +05:30
eKuG
b1b571ace9 feat: added implementation of trace operators 2025-06-22 15:38:42 +05:30
Ekansh Gupta
876f580f75 Merge branch 'main' into trace_operator_implementation 2025-06-20 15:45:15 +05:30
eKuG
7999f261ef feat: added implementation of trace operators 2025-06-20 14:41:12 +05:30
eKuG
66b8574f74 feat: added implementation of trace operators 2025-06-20 14:37:07 +05:30
eKuG
d7b8be11a4 feat: [draft] added implementation of trace operators 2025-06-20 00:18:27 +05:30
eKuG
aa3935cc31 feat: [draft] added implementation of trace operators 2025-06-20 00:08:52 +05:30
Ekansh Gupta
002c755ca5 Merge branch 'main' into trace_operator_implementation 2025-06-19 15:03:00 +05:30
eKuG
558739b4e7 feat: [draft] added implementation of trace operators 2025-06-19 00:08:41 +05:30
Ekansh Gupta
efdfa48ad0 Merge branch 'main' into trace_operator_implementation 2025-06-18 23:52:48 +05:30
eKuG
693c4451ee feat: [draft] added implementation of trace operators 2025-06-18 23:49:49 +05:30
107 changed files with 4567 additions and 994 deletions

View File

@@ -257,6 +257,7 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h
s.config.APIServer.Timeout.Max,
).Wrap)
r.Use(middleware.NewLogging(s.signoz.Instrumentation.Logger(), s.config.APIServer.Logging.ExcludedRoutes).Wrap)
r.Use(middleware.NewComment().Wrap)
apiHandler.RegisterRoutes(r, am)
apiHandler.RegisterLogsRoutes(r, am)

View File

@@ -5,7 +5,10 @@ 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 } from 'types/api/queryBuilder/queryBuilderData';
import {
IBuilderQuery,
IBuilderTraceOperator,
} from 'types/api/queryBuilder/queryBuilderData';
import {
BaseBuilderQuery,
FieldContext,
@@ -276,6 +279,103 @@ export function convertBuilderQueriesToV5(
);
}
function createTraceOperatorBaseSpec(
queryData: IBuilderTraceOperator,
requestType: RequestType,
panelType?: PANEL_TYPES,
): BaseBuilderQuery {
const nonEmptySelectColumns = (queryData.selectColumns as (
| BaseAutocompleteData
| TelemetryFieldKey
)[])?.filter((c) => ('key' in c ? c?.key : c?.name));
return {
stepInterval: queryData?.stepInterval || undefined,
groupBy:
queryData.groupBy?.length > 0
? queryData.groupBy.map(
(item: any): GroupByKey => ({
name: item.key,
fieldDataType: item?.dataType,
fieldContext: item?.type,
description: item?.description,
unit: item?.unit,
signal: item?.signal,
materialized: item?.materialized,
}),
)
: undefined,
limit:
panelType === PANEL_TYPES.TABLE || panelType === PANEL_TYPES.LIST
? queryData.limit || queryData.pageSize || undefined
: queryData.limit || undefined,
offset:
requestType === 'raw' || requestType === 'trace'
? queryData.offset
: undefined,
order:
queryData.orderBy?.length > 0
? queryData.orderBy.map(
(order: any): OrderBy => ({
key: {
name: order.columnName,
},
direction: order.order,
}),
)
: undefined,
legend: isEmpty(queryData.legend) ? undefined : queryData.legend,
having: isEmpty(queryData.having) ? undefined : (queryData?.having as Having),
selectFields: isEmpty(nonEmptySelectColumns)
? undefined
: nonEmptySelectColumns?.map(
(column: any): TelemetryFieldKey => ({
name: column.name ?? column.key,
fieldDataType:
column?.fieldDataType ?? (column?.dataType as FieldDataType),
fieldContext: column?.fieldContext ?? (column?.type as FieldContext),
signal: column?.signal ?? undefined,
}),
),
};
}
export function convertTraceOperatorToV5(
traceOperator: Record<string, IBuilderTraceOperator>,
requestType: RequestType,
panelType?: PANEL_TYPES,
): QueryEnvelope[] {
return Object.entries(traceOperator).map(
([queryName, traceOperatorData]): QueryEnvelope => {
const baseSpec = createTraceOperatorBaseSpec(
traceOperatorData,
requestType,
panelType,
);
let spec: QueryEnvelope['spec'];
// Skip aggregation for raw request type
const aggregations =
requestType === 'raw'
? undefined
: createAggregation(traceOperatorData, panelType);
spec = {
name: queryName,
returnSpansFrom: traceOperatorData.returnSpansFrom || '',
...baseSpec,
expression: traceOperatorData.expression || '',
aggregations: aggregations as TraceAggregation[],
};
return {
type: 'builder_trace_operator' as QueryType,
spec,
};
},
);
}
/**
* Converts PromQL queries to V5 format
*/
@@ -357,14 +457,27 @@ export const prepareQueryRangePayloadV5 = ({
switch (query.queryType) {
case EQueryType.QUERY_BUILDER: {
const { queryData: data, queryFormulas } = query.builder;
const { queryData: data, queryFormulas, queryTraceOperator } = 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
@@ -397,8 +510,36 @@ export const prepareQueryRangePayloadV5 = ({
}),
);
const traceOperatorQueries = convertTraceOperatorToV5(
currentTraceOperator.data,
requestType,
graphType,
);
// const traceOperatorQueries = Object.entries(currentTraceOperator.data).map(
// ([queryName, traceOperatorData]): QueryEnvelope => ({
// type: 'builder_trace_operator' as const,
// spec: {
// name: queryName,
// expression: traceOperatorData.expression || '',
// legend: isEmpty(traceOperatorData.legend)
// ? undefined
// : traceOperatorData.legend,
// limit: 10,
// order: traceOperatorData.orderBy?.map(
// // eslint-disable-next-line sonarjs/no-identical-functions
// (order: any): OrderBy => ({
// key: {
// name: order.columnName,
// },
// direction: order.order,
// }),
// ),
// },
// }),
// );
// Combine both types
queries = [...builderQueries, ...formulaQueries];
queries = [...builderQueries, ...formulaQueries, ...traceOperatorQueries];
break;
}
case EQueryType.PROM: {

View File

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

View File

@@ -5,11 +5,13 @@ 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,
@@ -18,6 +20,7 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
queryComponents,
isListViewPanel = false,
showOnlyWhereClause = false,
showTraceOperator = false,
version,
}: QueryBuilderProps): JSX.Element {
const {
@@ -25,6 +28,7 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
addNewBuilderQuery,
addNewFormula,
handleSetConfig,
addTraceOperator,
panelType,
initialDataSource,
} = useQueryBuilder();
@@ -54,6 +58,14 @@ 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 },
@@ -97,11 +109,45 @@ 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">
{isListViewPanel && (
{!isMultiQueryAllowed ? (
<QueryV2
ref={containerRef}
key={currentQuery.builder.queryData[0].queryName}
@@ -109,15 +155,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}
@@ -127,13 +173,16 @@ 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">
@@ -158,15 +207,25 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
</div>
)}
{!showOnlyWhereClause && !isListViewPanel && (
{shouldShowFooter && (
<QueryFooter
showAddFormula={showFormula}
addNewBuilderQuery={addNewBuilderQuery}
addNewFormula={addNewFormula}
addTraceOperator={addTraceOperator}
showAddTraceOperator={showTraceOperator && !traceOperator}
/>
)}
{shouldShowTraceOperator && (
<TraceOperator
isListViewPanel={isListViewPanel}
traceOperator={traceOperator as IBuilderTraceOperator}
/>
)}
</div>
{!showOnlyWhereClause && !isListViewPanel && (
{isMultiQueryAllowed && (
<div className="query-names-section">
{currentQuery.builder.queryData.map((query) => (
<div key={query.queryName} className="query-name">

View File

@@ -44,13 +44,14 @@
.lightMode {
.metrics-select-container {
.ant-select-selector {
border: 1px solid var(--bg-slate-300) !important;
border: 1px solid var(--bg-vanilla-300) !important;
background: var(--bg-vanilla-100);
color: var(--text-ink-100);
}
.ant-select-dropdown {
background: var(--bg-vanilla-100);
border: 1px solid var(--bg-vanilla-300) !important;
box-shadow: 0 3px 6px -4px rgba(0, 0, 0, 0.12),
0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 9px 28px 8px rgba(0, 0, 0, 0.05);
backdrop-filter: none;

View File

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

View File

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

View File

@@ -4,9 +4,15 @@ 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">
@@ -22,32 +28,62 @@ export default function QueryFooter({
</Tooltip>
</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}
{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>
}
>
Add Formula
</Button>
</Tooltip>
</div>
<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>
)}
</div>
</div>
);

View File

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

View File

@@ -82,14 +82,12 @@ function QuerySearch({
dataSource,
onRun,
signalSource,
isMetricsExplorer = false,
}: {
onChange: (value: string) => void;
queryData: IBuilderQuery;
dataSource: DataSource;
signalSource?: string;
onRun?: (query: string) => void;
isMetricsExplorer?: boolean;
}): JSX.Element {
const isDarkMode = useIsDarkMode();
const [query, setQuery] = useState<string>(queryData.filter?.expression || '');
@@ -210,8 +208,7 @@ function QuerySearch({
async (searchText?: string): Promise<void> => {
if (
dataSource === DataSource.METRICS &&
!queryData.aggregateAttribute?.key &&
!isMetricsExplorer
!queryData.aggregateAttribute?.key
) {
setKeySuggestions([]);
return;
@@ -252,7 +249,6 @@ function QuerySearch({
toggleSuggestions,
queryData.aggregateAttribute?.key,
signalSource,
isMetricsExplorer,
],
);
@@ -1457,7 +1453,6 @@ function QuerySearch({
QuerySearch.defaultProps = {
onRun: undefined,
signalSource: '',
isMetricsExplorer: false,
};
export default QuerySearch;

View File

@@ -26,9 +26,11 @@ 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();
@@ -108,11 +110,15 @@ export const QueryV2 = memo(function QueryV2({
ref={ref}
>
<div className="qb-content-section">
{!showOnlyWhereClause && (
{isMultiQueryAllowed && (
<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) ||
@@ -139,7 +145,30 @@ export const QueryV2 = memo(function QueryV2({
/>
</div>
{!isListViewPanel && (
{!isCollapsed &&
(showTraceOperator ||
(isListViewPanel && dataSource === DataSource.TRACES)) && (
<div className="qb-search-filter-container" style={{ flex: 1 }}>
<div className="query-search-container">
<QuerySearch
key={`query-search-${query.queryName}-${query.dataSource}`}
onChange={handleSearchChange}
queryData={query}
dataSource={dataSource}
signalSource={signalSource}
/>
</div>
{showSpanScopeSelector && (
<div className="traces-search-filter-container">
<div className="traces-search-filter-in">in</div>
<SpanScopeSelector query={query} />
</div>
)}
</div>
)}
{isMultiQueryAllowed && (
<Dropdown
className="query-actions-dropdown"
menu={{
@@ -181,28 +210,32 @@ export const QueryV2 = memo(function QueryV2({
</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>
{!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>
{showSpanScopeSelector && (
<div className="traces-search-filter-container">
<div className="traces-search-filter-in">in</div>
<SpanScopeSelector query={query} />
{showSpanScopeSelector && (
<div className="traces-search-filter-container">
<div className="traces-search-filter-in">in</div>
<SpanScopeSelector query={query} />
</div>
)}
</div>
)}
</div>
</div>
{!showOnlyWhereClause &&
!isListViewPanel &&
!showTraceOperator &&
dataSource !== DataSource.METRICS && (
<QueryAggregation
dataSource={dataSource}
@@ -225,7 +258,7 @@ export const QueryV2 = memo(function QueryV2({
/>
)}
{!showOnlyWhereClause && (
{!showOnlyWhereClause && !isListViewPanel && !showTraceOperator && (
<QueryAddOns
index={index}
query={query}

View File

@@ -0,0 +1,180 @@
.qb-trace-operator {
padding: 8px;
display: flex;
gap: 8px;
&.non-list-view {
padding-left: 40px;
position: relative;
&::before {
content: '';
position: absolute;
top: 50%;
transform: translateY(-50%);
left: 12px;
height: calc(100% - 48px);
width: 1px;
background: repeating-linear-gradient(
to bottom,
#1d212d,
#1d212d 4px,
transparent 4px,
transparent 8px
);
}
}
&-span-source-label {
display: flex;
align-items: center;
gap: 8px;
height: 24px;
&-query {
font-size: 14px;
font-weight: 400;
color: var(--bg-vanilla-100);
}
&-query-name {
width: 18px;
height: 18px;
display: grid;
place-content: center;
padding: 2px;
border-radius: 2px;
border: 1px solid rgba(242, 71, 105, 0.2);
background: rgba(242, 71, 105, 0.1);
color: var(--Sakura-400, #f56c87);
font-size: 12px;
}
}
&-arrow {
position: relative;
&::before {
content: '';
position: absolute;
top: 50%;
transform: translateY(-50%);
left: -26px;
height: 1px;
width: 20px;
background: repeating-linear-gradient(
to right,
#1d212d,
#1d212d 4px,
transparent 4px,
transparent 8px
);
}
&::after {
content: '';
position: absolute;
top: 50%;
left: -10px;
transform: translateY(-50%);
height: 4px;
width: 4px;
border-radius: 50%;
background-color: var(--bg-slate-400);
}
}
&-input {
width: 100%;
}
&-container {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
}
&-aggregation-container {
display: flex;
flex-direction: column;
gap: 8px;
}
&-add-ons-container {
width: 100%;
display: flex;
flex-direction: row;
gap: 16px;
}
&-add-ons-input {
position: relative;
display: flex;
align-items: center;
flex-direction: row;
border-radius: 2px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
&::before {
content: '';
position: absolute;
left: -16px;
top: 50%;
height: 1px;
width: 16px;
background-color: var(--bg-slate-400);
}
.label {
color: var(--bg-vanilla-400);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding: 0px 8px;
border-right: 1px solid var(--bg-slate-400);
}
}
}
.lightMode {
.qb-trace-operator {
&-arrow {
&::before {
background: repeating-linear-gradient(
to right,
var(--bg-vanilla-300),
var(--bg-vanilla-300) 4px,
transparent 4px,
transparent 8px
);
}
&::after {
background-color: var(--bg-vanilla-300);
}
}
&.non-list-view {
&::before {
background: repeating-linear-gradient(
to bottom,
var(--bg-vanilla-300),
var(--bg-vanilla-300) 4px,
transparent 4px,
transparent 8px
);
}
}
&-add-ons-input {
border: 1px solid var(--bg-vanilla-300) !important;
background: var(--bg-vanilla-100) !important;
.label {
color: var(--bg-ink-500) !important;
border-right: 1px solid var(--bg-vanilla-300) !important;
background: var(--bg-vanilla-100) !important;
}
}
}
}

View File

@@ -0,0 +1,157 @@
/* eslint-disable react/require-default-props */
/* eslint-disable sonarjs/no-duplicate-string */
import './TraceOperator.styles.scss';
import { Button, Select, Tooltip, Typography } from 'antd';
import cx from 'classnames';
import InputWithLabel from 'components/InputWithLabel/InputWithLabel';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import { Trash2 } from 'lucide-react';
import { useCallback, useMemo } from 'react';
import {
IBuilderQuery,
IBuilderTraceOperator,
} from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import QueryAddOns from '../QueryAddOns/QueryAddOns';
import QueryAggregation from '../QueryAggregation/QueryAggregation';
export default function TraceOperator({
traceOperator,
isListViewPanel = false,
}: {
traceOperator: IBuilderTraceOperator;
isListViewPanel?: boolean;
}): JSX.Element {
const { panelType, currentQuery, removeTraceOperator } = useQueryBuilder();
const { handleChangeQueryData } = useQueryOperations({
index: 0,
query: traceOperator,
entityVersion: '',
isForTraceOperator: true,
});
const handleTraceOperatorChange = useCallback(
(traceOperatorExpression: string) => {
handleChangeQueryData('expression', traceOperatorExpression);
},
[handleChangeQueryData],
);
const handleChangeAggregateEvery = useCallback(
(value: IBuilderQuery['stepInterval']) => {
handleChangeQueryData('stepInterval', value);
},
[handleChangeQueryData],
);
const handleChangeAggregation = useCallback(
(value: string) => {
handleChangeQueryData('aggregations', [
{
expression: value,
},
]);
},
[handleChangeQueryData],
);
const handleChangeSpanSource = useCallback(
(value: string) => {
handleChangeQueryData('returnSpansFrom', value);
},
[handleChangeQueryData],
);
const defaultSpanSource = useMemo(
() =>
traceOperator.returnSpansFrom ||
currentQuery.builder.queryData[0].queryName ||
'',
[currentQuery.builder.queryData, traceOperator?.returnSpansFrom],
);
const spanSourceOptions = useMemo(
() =>
currentQuery.builder.queryData.map((query) => ({
value: query.queryName,
label: (
<div className="qb-trace-operator-span-source-label">
<span className="qb-trace-operator-span-source-label-query">Query</span>
<p className="qb-trace-operator-span-source-label-query-name">
{query.queryName}
</p>
</div>
),
})),
[currentQuery.builder.queryData],
);
return (
<div className={cx('qb-trace-operator', !isListViewPanel && 'non-list-view')}>
<div className="qb-trace-operator-container">
<InputWithLabel
className={cx(
'qb-trace-operator-input',
!isListViewPanel && 'qb-trace-operator-arrow',
)}
initialValue={traceOperator?.expression || ''}
label="TRACES MATCHING"
placeholder="Add condition..."
type="text"
onChange={handleTraceOperatorChange}
/>
{!isListViewPanel && (
<div className="qb-trace-operator-aggregation-container">
<div className={cx(!isListViewPanel && 'qb-trace-operator-arrow')}>
<QueryAggregation
dataSource={DataSource.TRACES}
key={`query-search-${traceOperator.queryName}`}
panelType={panelType || undefined}
onAggregationIntervalChange={handleChangeAggregateEvery}
onChange={handleChangeAggregation}
queryData={traceOperator}
/>
</div>
<div
className={cx(
'qb-trace-operator-add-ons-container',
!isListViewPanel && 'qb-trace-operator-arrow',
)}
>
<QueryAddOns
index={0}
query={traceOperator}
version="v3"
isForTraceOperator
isListViewPanel={false}
showReduceTo={false}
panelType={panelType}
>
<div className="qb-trace-operator-add-ons-input">
<Typography.Text className="label">Using spans from</Typography.Text>
<Select
bordered={false}
defaultValue={defaultSpanSource}
style={{ minWidth: 120 }}
onChange={handleChangeSpanSource}
options={spanSourceOptions}
listItemHeight={24}
/>
</div>
</QueryAddOns>
</div>
</div>
)}
</div>
<Tooltip title="Remove Trace Operator" placement="topLeft">
<Button className="periscope-btn ghost" onClick={removeTraceOperator}>
<Trash2 size={14} />
</Button>
</Tooltip>
</div>
);
}

View File

@@ -0,0 +1,536 @@
/* eslint-disable sonarjs/no-duplicate-string */
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { convertFiltersToExpression } from '../utils';
describe('convertFiltersToExpression', () => {
it('should handle empty, null, and undefined inputs', () => {
// Test null and undefined
expect(convertFiltersToExpression(null as any)).toEqual({ expression: '' });
expect(convertFiltersToExpression(undefined as any)).toEqual({
expression: '',
});
// Test empty filters
expect(convertFiltersToExpression({ items: [], op: 'AND' })).toEqual({
expression: '',
});
expect(
convertFiltersToExpression({ items: undefined, op: 'AND' } as any),
).toEqual({ expression: '' });
});
it('should convert basic comparison operators with proper value formatting', () => {
const filters: TagFilter = {
items: [
{
id: '1',
key: { key: 'service', type: 'string' },
op: '=',
value: 'api-gateway',
},
{
id: '2',
key: { key: 'status', type: 'string' },
op: '!=',
value: 'error',
},
{
id: '3',
key: { key: 'duration', type: 'number' },
op: '>',
value: 100,
},
{
id: '4',
key: { key: 'count', type: 'number' },
op: '<=',
value: 50,
},
{
id: '5',
key: { key: 'is_active', type: 'boolean' },
op: '=',
value: true,
},
{
id: '6',
key: { key: 'enabled', type: 'boolean' },
op: '=',
value: false,
},
{
id: '7',
key: { key: 'count', type: 'number' },
op: '=',
value: 0,
},
{
id: '7',
key: { key: 'regex', type: 'string' },
op: 'regex',
value: '.*',
},
],
op: 'AND',
};
const result = convertFiltersToExpression(filters);
expect(result).toEqual({
expression:
"service = 'api-gateway' AND status != 'error' AND duration > 100 AND count <= 50 AND is_active = true AND enabled = false AND count = 0 AND regex REGEXP '.*'",
});
});
it('should handle string value formatting and escaping', () => {
const filters: TagFilter = {
items: [
{
id: '1',
key: { key: 'message', type: 'string' },
op: '=',
value: "user's data",
},
{
id: '2',
key: { key: 'description', type: 'string' },
op: '=',
value: '',
},
{
id: '3',
key: { key: 'path', type: 'string' },
op: '=',
value: '/api/v1/users',
},
],
op: 'AND',
};
const result = convertFiltersToExpression(filters);
expect(result).toEqual({
expression:
"message = 'user\\'s data' AND description = '' AND path = '/api/v1/users'",
});
});
it('should handle IN operator with various value types and array formatting', () => {
const filters: TagFilter = {
items: [
{
id: '1',
key: { key: 'service', type: 'string' },
op: 'IN',
value: ['api-gateway', 'user-service', 'auth-service'],
},
{
id: '2',
key: { key: 'status', type: 'string' },
op: 'IN',
value: 'success', // Single value should be converted to array
},
{
id: '3',
key: { key: 'tags', type: 'string' },
op: 'IN',
value: [], // Empty array
},
{
id: '4',
key: { key: 'name', type: 'string' },
op: 'IN',
value: ["John's", "Mary's", 'Bob'], // Values with quotes
},
],
op: 'AND',
};
const result = convertFiltersToExpression(filters);
expect(result).toEqual({
expression:
"service in ['api-gateway', 'user-service', 'auth-service'] AND status in ['success'] AND tags in [] AND name in ['John\\'s', 'Mary\\'s', 'Bob']",
});
});
it('should convert deprecated operators to their modern equivalents', () => {
const filters: TagFilter = {
items: [
{
id: '1',
key: { key: 'service', type: 'string' },
op: 'nin',
value: ['api-gateway', 'user-service'],
},
{
id: '2',
key: { key: 'message', type: 'string' },
op: 'nlike',
value: 'error',
},
{
id: '3',
key: { key: 'path', type: 'string' },
op: 'nregex',
value: '/api/.*',
},
{
id: '4',
key: { key: 'service', type: 'string' },
op: 'NIN', // Test case insensitivity
value: ['api-gateway'],
},
{
id: '5',
key: { key: 'user_id', type: 'string' },
op: 'nexists',
value: '',
},
{
id: '6',
key: { key: 'description', type: 'string' },
op: 'ncontains',
value: 'error',
},
{
id: '7',
key: { key: 'tags', type: 'string' },
op: 'nhas',
value: 'production',
},
{
id: '8',
key: { key: 'labels', type: 'string' },
op: 'nhasany',
value: ['env:prod', 'service:api'],
},
],
op: 'AND',
};
const result = convertFiltersToExpression(filters);
expect(result).toEqual({
expression:
"service NOT IN ['api-gateway', 'user-service'] AND message NOT LIKE 'error' AND path NOT REGEXP '/api/.*' AND service NOT IN ['api-gateway'] AND user_id NOT EXISTS AND description NOT CONTAINS 'error' AND NOT has(tags, 'production') AND NOT hasAny(labels, ['env:prod', 'service:api'])",
});
});
it('should handle non-value operators and function operators', () => {
const filters: TagFilter = {
items: [
{
id: '1',
key: { key: 'user_id', type: 'string' },
op: 'EXISTS',
value: '', // Value should be ignored for EXISTS
},
{
id: '2',
key: { key: 'user_id', type: 'string' },
op: 'EXISTS',
value: 'some-value', // Value should be ignored for EXISTS
},
{
id: '3',
key: { key: 'tags', type: 'string' },
op: 'has',
value: 'production',
},
{
id: '4',
key: { key: 'tags', type: 'string' },
op: 'hasAny',
value: ['production', 'staging'],
},
{
id: '5',
key: { key: 'tags', type: 'string' },
op: 'hasAll',
value: ['production', 'monitoring'],
},
],
op: 'AND',
};
const result = convertFiltersToExpression(filters);
expect(result).toEqual({
expression:
"user_id exists AND user_id exists AND has(tags, 'production') AND hasAny(tags, ['production', 'staging']) AND hasAll(tags, ['production', 'monitoring'])",
});
});
it('should filter out invalid filters and handle edge cases', () => {
const filters: TagFilter = {
items: [
{
id: '1',
key: { key: 'service', type: 'string' },
op: '=',
value: 'api-gateway',
},
{
id: '2',
key: undefined, // Invalid filter - should be skipped
op: '=',
value: 'error',
},
{
id: '3',
key: { key: '', type: 'string' }, // Invalid filter with empty key - should be skipped
op: '=',
value: 'test',
},
{
id: '4',
key: { key: 'status', type: 'string' },
op: ' = ', // Test whitespace handling
value: 'success',
},
{
id: '5',
key: { key: 'service', type: 'string' },
op: 'In', // Test mixed case handling
value: ['api-gateway'],
},
],
op: 'AND',
};
const result = convertFiltersToExpression(filters);
expect(result).toEqual({
expression:
"service = 'api-gateway' AND status = 'success' AND service in ['api-gateway']",
});
});
it('should handle complex mixed operator scenarios with proper joining', () => {
const filters: TagFilter = {
items: [
{
id: '1',
key: { key: 'service', type: 'string' },
op: 'IN',
value: ['api-gateway', 'user-service'],
},
{
id: '2',
key: { key: 'user_id', type: 'string' },
op: 'EXISTS',
value: '',
},
{
id: '3',
key: { key: 'tags', type: 'string' },
op: 'has',
value: 'production',
},
{
id: '4',
key: { key: 'duration', type: 'number' },
op: '>',
value: 100,
},
{
id: '5',
key: { key: 'status', type: 'string' },
op: 'nin',
value: ['error', 'timeout'],
},
{
id: '6',
key: { key: 'method', type: 'string' },
op: '=',
value: 'POST',
},
],
op: 'AND',
};
const result = convertFiltersToExpression(filters);
expect(result).toEqual({
expression:
"service in ['api-gateway', 'user-service'] AND user_id exists AND has(tags, 'production') AND duration > 100 AND status NOT IN ['error', 'timeout'] AND method = 'POST'",
});
});
it('should handle all numeric comparison operators and edge cases', () => {
const filters: TagFilter = {
items: [
{
id: '1',
key: { key: 'count', type: 'number' },
op: '=',
value: 0,
},
{
id: '2',
key: { key: 'score', type: 'number' },
op: '>',
value: 100,
},
{
id: '3',
key: { key: 'limit', type: 'number' },
op: '>=',
value: 50,
},
{
id: '4',
key: { key: 'threshold', type: 'number' },
op: '<',
value: 1000,
},
{
id: '5',
key: { key: 'max_value', type: 'number' },
op: '<=',
value: 999,
},
{
id: '6',
key: { key: 'values', type: 'string' },
op: 'IN',
value: ['1', '2', '3', '4', '5'],
},
],
op: 'AND',
};
const result = convertFiltersToExpression(filters);
expect(result).toEqual({
expression:
"count = 0 AND score > 100 AND limit >= 50 AND threshold < 1000 AND max_value <= 999 AND values in ['1', '2', '3', '4', '5']",
});
});
it('should handle boolean values and string comparisons with special characters', () => {
const filters: TagFilter = {
items: [
{
id: '1',
key: { key: 'is_active', type: 'boolean' },
op: '=',
value: true,
},
{
id: '2',
key: { key: 'is_deleted', type: 'boolean' },
op: '=',
value: false,
},
{
id: '3',
key: { key: 'email', type: 'string' },
op: '=',
value: 'user@example.com',
},
{
id: '4',
key: { key: 'description', type: 'string' },
op: '=',
value: 'Contains "quotes" and \'apostrophes\'',
},
{
id: '5',
key: { key: 'path', type: 'string' },
op: '=',
value: '/api/v1/users/123?filter=true',
},
],
op: 'AND',
};
const result = convertFiltersToExpression(filters);
expect(result).toEqual({
expression:
"is_active = true AND is_deleted = false AND email = 'user@example.com' AND description = 'Contains \"quotes\" and \\'apostrophes\\'' AND path = '/api/v1/users/123?filter=true'",
});
});
it('should handle all function operators and complex array scenarios', () => {
const filters: TagFilter = {
items: [
{
id: '1',
key: { key: 'tags', type: 'string' },
op: 'has',
value: 'production',
},
{
id: '2',
key: { key: 'labels', type: 'string' },
op: 'hasAny',
value: ['env:prod', 'service:api'],
},
{
id: '3',
key: { key: 'metadata', type: 'string' },
op: 'hasAll',
value: ['version:1.0', 'team:backend'],
},
{
id: '4',
key: { key: 'services', type: 'string' },
op: 'IN',
value: ['api-gateway', 'user-service', 'auth-service', 'payment-service'],
},
{
id: '5',
key: { key: 'excluded_services', type: 'string' },
op: 'nin',
value: ['legacy-service', 'deprecated-service'],
},
{
id: '6',
key: { key: 'status_codes', type: 'string' },
op: 'IN',
value: ['200', '201', '400', '500'],
},
],
op: 'AND',
};
const result = convertFiltersToExpression(filters);
expect(result).toEqual({
expression:
"has(tags, 'production') AND hasAny(labels, ['env:prod', 'service:api']) AND hasAll(metadata, ['version:1.0', 'team:backend']) AND services in ['api-gateway', 'user-service', 'auth-service', 'payment-service'] AND excluded_services NOT IN ['legacy-service', 'deprecated-service'] AND status_codes in ['200', '201', '400', '500']",
});
});
it('should handle specific deprecated operators: nhas, ncontains, nexists', () => {
const filters: TagFilter = {
items: [
{
id: '1',
key: { key: 'user_id', type: 'string' },
op: 'nexists',
value: '',
},
{
id: '2',
key: { key: 'description', type: 'string' },
op: 'ncontains',
value: 'error',
},
{
id: '3',
key: { key: 'tags', type: 'string' },
op: 'nhas',
value: 'production',
},
{
id: '4',
key: { key: 'labels', type: 'string' },
op: 'nhasany',
value: ['env:prod', 'service:api'],
},
],
op: 'AND',
};
const result = convertFiltersToExpression(filters);
expect(result).toEqual({
expression:
"user_id NOT EXISTS AND description NOT CONTAINS 'error' AND NOT has(tags, 'production') AND NOT hasAny(labels, ['env:prod', 'service:api'])",
});
});
});

View File

@@ -1,6 +1,10 @@
/* eslint-disable sonarjs/cognitive-complexity */
import { createAggregation } from 'api/v5/queryRange/prepareQueryRangePayloadV5';
import { OPERATORS } from 'constants/antlrQueryConstants';
import {
DEPRECATED_OPERATORS_MAP,
OPERATORS,
QUERY_BUILDER_FUNCTIONS,
} from 'constants/antlrQueryConstants';
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
import { cloneDeep } from 'lodash-es';
import { IQueryPair } from 'types/antlrQueryTypes';
@@ -21,7 +25,7 @@ import { EQueryType } from 'types/common/dashboard';
import { DataSource } from 'types/common/queryBuilder';
import { extractQueryPairs } from 'utils/queryContextUtils';
import { unquote } from 'utils/stringUtils';
import { isFunctionOperator } from 'utils/tokenUtils';
import { isFunctionOperator, isNonValueOperator } from 'utils/tokenUtils';
import { v4 as uuid } from 'uuid';
/**
@@ -87,12 +91,32 @@ export const convertFiltersToExpression = (
return '';
}
if (isFunctionOperator(op)) {
return `${op}(${key.key}, ${value})`;
let operator = op.trim().toLowerCase();
if (Object.keys(DEPRECATED_OPERATORS_MAP).includes(operator)) {
operator =
DEPRECATED_OPERATORS_MAP[
operator as keyof typeof DEPRECATED_OPERATORS_MAP
];
}
const formattedValue = formatValueForExpression(value, op);
return `${key.key} ${op} ${formattedValue}`;
if (isNonValueOperator(operator)) {
return `${key.key} ${operator}`;
}
if (isFunctionOperator(operator)) {
// Get the proper function name from QUERY_BUILDER_FUNCTIONS
const functionOperators = Object.values(QUERY_BUILDER_FUNCTIONS);
const properFunctionName =
functionOperators.find(
(func: string) => func.toLowerCase() === operator.toLowerCase(),
) || operator;
const formattedValue = formatValueForExpression(value, operator);
return `${properFunctionName}(${key.key}, ${formattedValue})`;
}
const formattedValue = formatValueForExpression(value, operator);
return `${key.key} ${operator} ${formattedValue}`;
})
.filter((expression) => expression !== ''); // Remove empty expressions
@@ -117,7 +141,6 @@ export const convertExpressionToFilters = (
if (!expression) return [];
const queryPairs = extractQueryPairs(expression);
const filters: TagFilterItem[] = [];
queryPairs.forEach((pair) => {
@@ -145,19 +168,36 @@ export const convertFiltersToExpressionWithExistingQuery = (
filters: TagFilter,
existingQuery: string | undefined,
): { filters: TagFilter; filter: { expression: string } } => {
// Check for deprecated operators and replace them with new operators
const updatedFilters = cloneDeep(filters);
// Replace deprecated operators in filter items
if (updatedFilters?.items) {
updatedFilters.items = updatedFilters.items.map((item) => {
const opLower = item.op?.toLowerCase();
if (Object.keys(DEPRECATED_OPERATORS_MAP).includes(opLower)) {
return {
...item,
op: DEPRECATED_OPERATORS_MAP[
opLower as keyof typeof DEPRECATED_OPERATORS_MAP
].toLowerCase(),
};
}
return item;
});
}
if (!existingQuery) {
// If no existing query, return filters with a newly generated expression
return {
filters,
filter: convertFiltersToExpression(filters),
filters: updatedFilters,
filter: convertFiltersToExpression(updatedFilters),
};
}
// Extract query pairs from the existing query
const queryPairs = extractQueryPairs(existingQuery.trim());
let queryPairsMap: Map<string, IQueryPair> = new Map();
const updatedFilters = cloneDeep(filters); // Clone filters to avoid direct mutation
const nonExistingFilters: TagFilterItem[] = [];
let modifiedQuery = existingQuery; // We'll modify this query as we proceed
const visitedPairs: Set<string> = new Set(); // Set to track visited query pairs

View File

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

View File

@@ -1,3 +1,5 @@
/* eslint-disable @typescript-eslint/naming-convention */
export const OPERATORS = {
IN: 'IN',
LIKE: 'LIKE',
@@ -21,6 +23,44 @@ export const QUERY_BUILDER_FUNCTIONS = {
HASALL: 'hasAll',
};
export function negateOperator(operatorOrFunction: string): string {
// Special cases for equals/not equals
if (operatorOrFunction === OPERATORS['=']) {
return OPERATORS['!='];
}
if (operatorOrFunction === OPERATORS['!=']) {
return OPERATORS['='];
}
// For all other operators and functions, add NOT in front
return `${OPERATORS.NOT} ${operatorOrFunction}`;
}
export enum DEPRECATED_OPERATORS {
REGEX = 'regex',
NIN = 'nin',
NREGEX = 'nregex',
NLIKE = 'nlike',
NILIKE = 'nilike',
NEXTISTS = 'nexists',
NCONTAINS = 'ncontains',
NHAS = 'nhas',
NHASANY = 'nhasany',
NHASALL = 'nhasall',
}
export const DEPRECATED_OPERATORS_MAP = {
[DEPRECATED_OPERATORS.REGEX]: OPERATORS.REGEXP,
[DEPRECATED_OPERATORS.NIN]: negateOperator(OPERATORS.IN),
[DEPRECATED_OPERATORS.NREGEX]: negateOperator(OPERATORS.REGEXP),
[DEPRECATED_OPERATORS.NLIKE]: negateOperator(OPERATORS.LIKE),
[DEPRECATED_OPERATORS.NILIKE]: negateOperator(OPERATORS.ILIKE),
[DEPRECATED_OPERATORS.NEXTISTS]: negateOperator(OPERATORS.EXISTS),
[DEPRECATED_OPERATORS.NCONTAINS]: negateOperator(OPERATORS.CONTAINS),
[DEPRECATED_OPERATORS.NHAS]: negateOperator(QUERY_BUILDER_FUNCTIONS.HAS),
[DEPRECATED_OPERATORS.NHASANY]: negateOperator(QUERY_BUILDER_FUNCTIONS.HASANY),
[DEPRECATED_OPERATORS.NHASALL]: negateOperator(QUERY_BUILDER_FUNCTIONS.HASALL),
};
export const NON_VALUE_OPERATORS = [OPERATORS.EXISTS];
// eslint-disable-next-line @typescript-eslint/naming-convention
@@ -82,15 +122,3 @@ export const queryOperatorSuggestions = [
{ label: OPERATORS.NOT, type: 'operator', info: 'Not' },
...negationQueryOperatorSuggestions,
];
export function negateOperator(operatorOrFunction: string): string {
// Special cases for equals/not equals
if (operatorOrFunction === OPERATORS['=']) {
return OPERATORS['!='];
}
if (operatorOrFunction === OPERATORS['!=']) {
return OPERATORS['='];
}
// For all other operators and functions, add NOT in front
return `${OPERATORS.NOT} ${operatorOrFunction}`;
}

View File

@@ -12,6 +12,7 @@ import {
HavingForm,
IBuilderFormula,
IBuilderQuery,
IBuilderTraceOperator,
IClickHouseQuery,
IPromQLQuery,
Query,
@@ -50,6 +51,8 @@ import {
export const MAX_FORMULAS = 20;
export const MAX_QUERIES = 26;
export const TRACE_OPERATOR_QUERY_NAME = 'T1';
export const idDivider = '--';
export const selectValueDivider = '__';
@@ -265,6 +268,11 @@ 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: '',
@@ -282,6 +290,7 @@ export const initialClickHouseData: IClickHouseQuery = {
export const initialQueryBuilderData: QueryBuilderData = {
queryData: [initialQueryBuilderFormValues],
queryFormulas: [],
queryTraceOperator: [],
};
export const initialSingleQueryMap: Record<

View File

@@ -54,6 +54,7 @@ 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,6 +150,7 @@ 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

@@ -256,7 +256,6 @@ function LogsExplorerViewsContainer({
} = useGetExplorerQueryRange(
listChartQuery,
PANEL_TYPES.TIME_SERIES,
// ENTITY_VERSION_V4,
ENTITY_VERSION_V5,
{
enabled:
@@ -279,7 +278,6 @@ function LogsExplorerViewsContainer({
} = useGetExplorerQueryRange(
requestData,
panelType,
// ENTITY_VERSION_V4,
ENTITY_VERSION_V5,
{
keepPreviousData: true,

View File

@@ -141,6 +141,19 @@
background: var(--bg-vanilla-500);
}
}
.meter-explorer-content-section {
.explore-content {
.time-series-view-panel {
background: var(--bg-vanilla-100);
border-color: var(--bg-vanilla-300);
}
}
}
.meter-explorer-quick-filters-section {
border-right: 1px solid var(--bg-vanilla-300);
}
}
}

View File

@@ -33,7 +33,7 @@ function ExpandedView({
options,
spaceAggregationSeriesMap,
step,
appliedMetricInspectionOptions,
metricInspectionOptions,
timeAggregatedSeriesMap,
}: ExpandedViewProps): JSX.Element {
const [
@@ -44,17 +44,17 @@ function ExpandedView({
useEffect(() => {
logEvent(MetricsExplorerEvents.InspectPointClicked, {
[MetricsExplorerEventKeys.Modal]: 'inspect',
[MetricsExplorerEventKeys.Filters]: appliedMetricInspectionOptions.filters,
[MetricsExplorerEventKeys.Filters]: metricInspectionOptions.filters,
[MetricsExplorerEventKeys.TimeAggregationInterval]:
appliedMetricInspectionOptions.timeAggregationInterval,
metricInspectionOptions.timeAggregationInterval,
[MetricsExplorerEventKeys.TimeAggregationOption]:
appliedMetricInspectionOptions.timeAggregationOption,
metricInspectionOptions.timeAggregationOption,
[MetricsExplorerEventKeys.SpaceAggregationOption]:
appliedMetricInspectionOptions.spaceAggregationOption,
metricInspectionOptions.spaceAggregationOption,
[MetricsExplorerEventKeys.SpaceAggregationLabels]:
appliedMetricInspectionOptions.spaceAggregationLabels,
metricInspectionOptions.spaceAggregationLabels,
});
}, [appliedMetricInspectionOptions]);
}, [metricInspectionOptions]);
useEffect(() => {
if (step !== InspectionStep.COMPLETED) {
@@ -167,7 +167,7 @@ function ExpandedView({
<Typography.Text strong>
{`${absoluteValue} is the ${
SPACE_AGGREGATION_OPTIONS_FOR_EXPANDED_VIEW[
appliedMetricInspectionOptions.spaceAggregationOption ??
metricInspectionOptions.spaceAggregationOption ??
SpaceAggregationOptions.SUM_BY
]
} of`}
@@ -240,7 +240,7 @@ function ExpandedView({
)?.value ?? options?.value
} is the ${
TIME_AGGREGATION_OPTIONS[
appliedMetricInspectionOptions.timeAggregationOption ??
metricInspectionOptions.timeAggregationOption ??
TimeAggregationOptions.SUM
]
} of`
@@ -299,7 +299,7 @@ function ExpandedView({
<Typography.Text strong>
{`${absoluteValue} is the ${
TIME_AGGREGATION_OPTIONS[
appliedMetricInspectionOptions.timeAggregationOption ??
metricInspectionOptions.timeAggregationOption ??
TimeAggregationOptions.SUM
]
} of`}

View File

@@ -29,7 +29,7 @@ function GraphView({
popoverOptions,
setShowExpandedView,
setExpandedViewOptions,
appliedMetricInspectionOptions,
metricInspectionOptions,
isInspectMetricsRefetching,
}: GraphViewProps): JSX.Element {
const isDarkMode = useIsDarkMode();
@@ -233,7 +233,7 @@ function GraphView({
inspectMetricsTimeSeries={inspectMetricsTimeSeries}
setShowExpandedView={setShowExpandedView}
setExpandedViewOptions={setExpandedViewOptions}
appliedMetricInspectionOptions={appliedMetricInspectionOptions}
metricInspectionOptions={metricInspectionOptions}
isInspectMetricsRefetching={isInspectMetricsRefetching}
/>
)}
@@ -255,7 +255,7 @@ function GraphView({
<HoverPopover
options={hoverPopoverOptions}
step={inspectionStep}
appliedMetricInspectionOptions={appliedMetricInspectionOptions}
metricInspectionOptions={metricInspectionOptions}
/>
)}
</div>

View File

@@ -122,10 +122,6 @@
gap: 4px;
.inspect-metrics-query-builder-header {
display: flex;
align-items: center;
justify-content: space-between;
.query-builder-button-label {
display: flex;
align-items: center;

View File

@@ -11,7 +11,6 @@ import { useIsDarkMode } from 'hooks/useDarkMode';
import { Compass } from 'lucide-react';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { MetricsExplorerEventKeys, MetricsExplorerEvents } from '../events';
@@ -25,7 +24,6 @@ import {
MetricInspectionAction,
} from './types';
import { useInspectMetrics } from './useInspectMetrics';
import { useMetricName } from './utils';
function Inspect({
metricName: defaultMetricName,
@@ -33,12 +31,7 @@ function Inspect({
onClose,
}: InspectProps): JSX.Element {
const isDarkMode = useIsDarkMode();
const {
currentMetricName,
setCurrentMetricName,
appliedMetricName,
setAppliedMetricName,
} = useMetricName(defaultMetricName);
const [metricName, setMetricName] = useState<string | null>(defaultMetricName);
const [
popoverOptions,
setPopoverOptions,
@@ -49,12 +42,9 @@ function Inspect({
] = useState<GraphPopoverOptions | null>(null);
const [showExpandedView, setShowExpandedView] = useState(false);
const { data: metricDetailsData } = useGetMetricDetails(
appliedMetricName ?? '',
{
enabled: !!appliedMetricName,
},
);
const { data: metricDetailsData } = useGetMetricDetails(metricName ?? '', {
enabled: !!metricName,
});
const { currentQuery } = useQueryBuilder();
const { handleChangeQueryData } = useQueryOperations({
@@ -107,16 +97,25 @@ function Inspect({
aggregatedTimeSeries,
timeAggregatedSeriesMap,
reset,
} = useInspectMetrics(appliedMetricName);
} = useInspectMetrics(metricName);
const handleDispatchMetricInspectionOptions = useCallback(
(action: MetricInspectionAction): void => {
dispatchMetricInspectionOptions(action);
logEvent(MetricsExplorerEvents.InspectQueryChanged, {
[MetricsExplorerEventKeys.Modal]: 'inspect',
[MetricsExplorerEventKeys.Filters]: metricInspectionOptions.filters,
[MetricsExplorerEventKeys.TimeAggregationInterval]:
metricInspectionOptions.timeAggregationInterval,
[MetricsExplorerEventKeys.TimeAggregationOption]:
metricInspectionOptions.timeAggregationOption,
[MetricsExplorerEventKeys.SpaceAggregationOption]:
metricInspectionOptions.spaceAggregationOption,
[MetricsExplorerEventKeys.SpaceAggregationLabels]:
metricInspectionOptions.spaceAggregationLabels,
});
},
[dispatchMetricInspectionOptions],
[dispatchMetricInspectionOptions, metricInspectionOptions],
);
const selectedMetricType = useMemo(
@@ -129,30 +128,18 @@ function Inspect({
[metricDetailsData],
);
const aggregateAttribute = useMemo(
() => ({
key: currentMetricName ?? '',
dataType: DataTypes.String,
type: selectedMetricType as string,
isColumn: true,
isJSON: false,
id: `${currentMetricName}--${DataTypes.String}--${selectedMetricType}--true`,
}),
[currentMetricName, selectedMetricType],
);
const [currentQueryData, setCurrentQueryData] = useState<IBuilderQuery>({
...searchQuery,
aggregateAttribute,
});
const resetInspection = useCallback(() => {
setShowExpandedView(false);
setPopoverOptions(null);
setExpandedViewOptions(null);
setCurrentQueryData(searchQuery as IBuilderQuery);
reset();
}, [reset, searchQuery]);
}, [reset]);
// Reset inspection when the selected metric changes
useEffect(() => {
resetInspection();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [metricName]);
// Hide expanded view whenever inspection step changes
useEffect(() => {
@@ -206,7 +193,7 @@ function Inspect({
inspectMetricsTimeSeries={aggregatedTimeSeries}
formattedInspectMetricsTimeSeries={formattedInspectMetricsTimeSeries}
resetInspection={resetInspection}
metricName={appliedMetricName}
metricName={metricName}
metricUnit={selectedMetricUnit}
metricType={selectedMetricType}
spaceAggregationSeriesMap={spaceAggregationSeriesMap}
@@ -216,20 +203,19 @@ function Inspect({
showExpandedView={showExpandedView}
setExpandedViewOptions={setExpandedViewOptions}
popoverOptions={popoverOptions}
appliedMetricInspectionOptions={metricInspectionOptions.appliedOptions}
metricInspectionOptions={metricInspectionOptions}
isInspectMetricsRefetching={isInspectMetricsRefetching}
/>
<QueryBuilder
currentMetricName={currentMetricName}
setCurrentMetricName={setCurrentMetricName}
setAppliedMetricName={setAppliedMetricName}
metricName={metricName}
metricType={selectedMetricType}
setMetricName={setMetricName}
spaceAggregationLabels={spaceAggregationLabels}
currentMetricInspectionOptions={metricInspectionOptions.currentOptions}
metricInspectionOptions={metricInspectionOptions}
dispatchMetricInspectionOptions={handleDispatchMetricInspectionOptions}
inspectionStep={inspectionStep}
inspectMetricsTimeSeries={inspectMetricsTimeSeries}
currentQuery={currentQueryData}
setCurrentQuery={setCurrentQueryData}
searchQuery={searchQuery as IBuilderQuery}
/>
</div>
<div className="inspect-metrics-content-second-col">
@@ -242,7 +228,7 @@ function Inspect({
options={expandedViewOptions}
spaceAggregationSeriesMap={spaceAggregationSeriesMap}
step={inspectionStep}
appliedMetricInspectionOptions={metricInspectionOptions.appliedOptions}
metricInspectionOptions={metricInspectionOptions}
timeAggregatedSeriesMap={timeAggregatedSeriesMap}
/>
)}
@@ -258,21 +244,17 @@ function Inspect({
aggregatedTimeSeries,
formattedInspectMetricsTimeSeries,
resetInspection,
appliedMetricName,
metricName,
selectedMetricUnit,
selectedMetricType,
spaceAggregationSeriesMap,
inspectionStep,
showExpandedView,
popoverOptions,
metricInspectionOptions.appliedOptions,
metricInspectionOptions.currentOptions,
currentMetricName,
setCurrentMetricName,
setAppliedMetricName,
metricInspectionOptions,
spaceAggregationLabels,
handleDispatchMetricInspectionOptions,
currentQueryData,
searchQuery,
expandedViewOptions,
timeAggregatedSeriesMap,
]);

View File

@@ -1,6 +1,5 @@
import { Button, Card } from 'antd';
import { Atom, Play } from 'lucide-react';
import { useCallback } from 'react';
import { Atom } from 'lucide-react';
import { QueryBuilderProps } from './types';
import {
@@ -11,24 +10,16 @@ import {
} from './utils';
function QueryBuilder({
currentMetricName,
setCurrentMetricName,
setAppliedMetricName,
metricName,
setMetricName,
spaceAggregationLabels,
currentMetricInspectionOptions,
metricInspectionOptions,
dispatchMetricInspectionOptions,
inspectionStep,
inspectMetricsTimeSeries,
currentQuery,
setCurrentQuery,
searchQuery,
metricType,
}: QueryBuilderProps): JSX.Element {
const applyInspectionOptions = useCallback(() => {
setAppliedMetricName(currentMetricName ?? '');
dispatchMetricInspectionOptions({
type: 'APPLY_INSPECTION_OPTIONS',
});
}, [currentMetricName, setAppliedMetricName, dispatchMetricInspectionOptions]);
return (
<div className="inspect-metrics-query-builder">
<div className="inspect-metrics-query-builder-header">
@@ -40,36 +31,25 @@ function QueryBuilder({
>
Query Builder
</Button>
<Button
type="primary"
className="stage-run-query"
icon={<Play size={14} />}
onClick={applyInspectionOptions}
data-testid="apply-query-button"
>
Stage & Run Query
</Button>
</div>
<Card className="inspect-metrics-query-builder-content">
<MetricNameSearch
currentMetricName={currentMetricName}
setCurrentMetricName={setCurrentMetricName}
/>
<MetricNameSearch metricName={metricName} setMetricName={setMetricName} />
<MetricFilters
dispatchMetricInspectionOptions={dispatchMetricInspectionOptions}
currentQuery={currentQuery}
setCurrentQuery={setCurrentQuery}
searchQuery={searchQuery}
metricName={metricName}
metricType={metricType || null}
/>
<MetricTimeAggregation
inspectionStep={inspectionStep}
currentMetricInspectionOptions={currentMetricInspectionOptions}
metricInspectionOptions={metricInspectionOptions}
dispatchMetricInspectionOptions={dispatchMetricInspectionOptions}
inspectMetricsTimeSeries={inspectMetricsTimeSeries}
/>
<MetricSpaceAggregation
inspectionStep={inspectionStep}
spaceAggregationLabels={spaceAggregationLabels}
currentMetricInspectionOptions={currentMetricInspectionOptions}
metricInspectionOptions={metricInspectionOptions}
dispatchMetricInspectionOptions={dispatchMetricInspectionOptions}
/>
</Card>

View File

@@ -11,13 +11,13 @@ function TableView({
setShowExpandedView,
setExpandedViewOptions,
isInspectMetricsRefetching,
appliedMetricInspectionOptions,
metricInspectionOptions,
}: TableViewProps): JSX.Element {
const isSpaceAggregatedWithoutLabel = useMemo(
() =>
!!appliedMetricInspectionOptions.spaceAggregationOption &&
appliedMetricInspectionOptions.spaceAggregationLabels.length === 0,
[appliedMetricInspectionOptions],
!!metricInspectionOptions.spaceAggregationOption &&
metricInspectionOptions.spaceAggregationLabels.length === 0,
[metricInspectionOptions],
);
const labelKeys = useMemo(() => {
if (isSpaceAggregatedWithoutLabel) {

View File

@@ -10,7 +10,7 @@ import ExpandedView from '../ExpandedView';
import {
GraphPopoverData,
InspectionStep,
InspectOptions,
MetricInspectionOptions,
SpaceAggregationOptions,
TimeAggregationOptions,
} from '../types';
@@ -62,7 +62,7 @@ describe('ExpandedView', () => {
],
]);
const mockMetricInspectionOptions: InspectOptions = {
const mockMetricInspectionOptions: MetricInspectionOptions = {
timeAggregationOption: TimeAggregationOptions.MAX,
timeAggregationInterval: 60,
spaceAggregationOption: SpaceAggregationOptions.MAX_BY,
@@ -79,7 +79,7 @@ describe('ExpandedView', () => {
options={mockOptions}
spaceAggregationSeriesMap={mockSpaceAggregationSeriesMap}
step={InspectionStep.TIME_AGGREGATION}
appliedMetricInspectionOptions={mockMetricInspectionOptions}
metricInspectionOptions={mockMetricInspectionOptions}
timeAggregatedSeriesMap={mockTimeAggregatedSeriesMap}
/>,
);
@@ -96,7 +96,7 @@ describe('ExpandedView', () => {
options={mockOptions}
spaceAggregationSeriesMap={mockSpaceAggregationSeriesMap}
step={InspectionStep.SPACE_AGGREGATION}
appliedMetricInspectionOptions={{
metricInspectionOptions={{
...mockMetricInspectionOptions,
timeAggregationInterval: TIME_AGGREGATION_INTERVAL,
}}
@@ -127,7 +127,7 @@ describe('ExpandedView', () => {
options={mockOptions}
spaceAggregationSeriesMap={mockSpaceAggregationSeriesMap}
step={InspectionStep.COMPLETED}
appliedMetricInspectionOptions={mockMetricInspectionOptions}
metricInspectionOptions={mockMetricInspectionOptions}
timeAggregatedSeriesMap={mockTimeAggregatedSeriesMap}
/>,
);
@@ -153,7 +153,7 @@ describe('ExpandedView', () => {
options={mockOptions}
spaceAggregationSeriesMap={mockSpaceAggregationSeriesMap}
step={InspectionStep.TIME_AGGREGATION}
appliedMetricInspectionOptions={mockMetricInspectionOptions}
metricInspectionOptions={mockMetricInspectionOptions}
timeAggregatedSeriesMap={mockTimeAggregatedSeriesMap}
/>,
);

View File

@@ -60,7 +60,7 @@ describe('GraphView', () => {
setExpandedViewOptions: jest.fn(),
resetInspection: jest.fn(),
showExpandedView: false,
appliedMetricInspectionOptions: {
metricInspectionOptions: {
timeAggregationInterval: 60,
spaceAggregationOption: SpaceAggregationOptions.MAX_BY,
spaceAggregationLabels: ['host_name'],

View File

@@ -1,5 +1,5 @@
/* eslint-disable react/jsx-props-no-spreading */
import { fireEvent, render, screen } from '@testing-library/react';
import { render, screen } from '@testing-library/react';
import { MetricType } from 'api/metricsExplorer/getMetricsList';
import * as appContextHooks from 'providers/App/App';
import { QueryClient, QueryClientProvider } from 'react-query';
@@ -22,27 +22,6 @@ jest.mock('react-router-dom', () => ({
}),
}));
jest.mock('container/QueryBuilder/filters', () => ({
AggregatorFilter: ({ onSelect, onChange, defaultValue }: any): JSX.Element => (
<div data-testid="mock-aggregator-filter">
<input
data-testid="metric-name-input"
defaultValue={defaultValue}
onChange={(e: React.ChangeEvent<HTMLInputElement>): void =>
onChange({ key: e.target.value })
}
/>
<button
type="button"
data-testid="select-metric-button"
onClick={(): void => onSelect({ key: 'test_metric_2' })}
>
Select Metric
</button>
</div>
),
}));
jest.spyOn(appContextHooks, 'useAppContext').mockReturnValue({
user: {
role: 'admin',
@@ -69,16 +48,12 @@ jest.spyOn(appContextHooks, 'useAppContext').mockReturnValue({
const queryClient = new QueryClient();
const mockSetCurrentMetricName = jest.fn();
const mockSetAppliedMetricName = jest.fn();
describe('QueryBuilder', () => {
const defaultProps = {
currentMetricName: 'test_metric',
setCurrentMetricName: mockSetCurrentMetricName,
setAppliedMetricName: mockSetAppliedMetricName,
metricName: 'test_metric',
setMetricName: jest.fn(),
spaceAggregationLabels: ['label1', 'label2'],
currentMetricInspectionOptions: {
metricInspectionOptions: {
timeAggregationInterval: 60,
timeAggregationOption: TimeAggregationOptions.AVG,
spaceAggregationLabels: [],
@@ -92,13 +67,12 @@ describe('QueryBuilder', () => {
metricType: MetricType.SUM,
inspectionStep: InspectionStep.TIME_AGGREGATION,
inspectMetricsTimeSeries: [],
currentQuery: {
searchQuery: {
filters: {
items: [],
op: 'and',
},
} as any,
setCurrentQuery: jest.fn(),
};
beforeEach(() => {
@@ -159,57 +133,4 @@ describe('QueryBuilder', () => {
);
expect(screen.getByTestId('metric-space-aggregation')).toBeInTheDocument();
});
it('should call setCurrentMetricName when metric name is selected', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<QueryBuilder {...defaultProps} />
</Provider>
</QueryClientProvider>,
);
const metricNameSearch = screen.getByTestId('metric-name-search');
expect(metricNameSearch).toBeInTheDocument();
expect(screen.getByText('From')).toBeInTheDocument();
const selectButton = screen.getByTestId('select-metric-button');
fireEvent.click(selectButton);
expect(mockSetCurrentMetricName).toHaveBeenCalledWith('test_metric_2');
});
it('should call setAppliedMetricName when query is applied', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<QueryBuilder {...defaultProps} />
</Provider>
</QueryClientProvider>,
);
const applyQueryButton = screen.getByTestId('apply-query-button');
fireEvent.click(applyQueryButton);
expect(mockSetCurrentMetricName).toHaveBeenCalledTimes(0);
expect(mockSetAppliedMetricName).toHaveBeenCalledWith('test_metric');
});
it('should apply inspect options when query is applied', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<QueryBuilder {...defaultProps} />
</Provider>
</QueryClientProvider>,
);
const applyQueryButton = screen.getByTestId('apply-query-button');
fireEvent.click(applyQueryButton);
expect(defaultProps.dispatchMetricInspectionOptions).toHaveBeenCalledWith({
type: 'APPLY_INSPECTION_OPTIONS',
});
});
});

View File

@@ -49,7 +49,7 @@ describe('TableView', () => {
inspectMetricsTimeSeries: mockTimeSeries,
setShowExpandedView: jest.fn(),
setExpandedViewOptions: jest.fn(),
appliedMetricInspectionOptions: {
metricInspectionOptions: {
timeAggregationInterval: 60,
timeAggregationOption: TimeAggregationOptions.MAX,
spaceAggregationOption: SpaceAggregationOptions.MAX_BY,

View File

@@ -72,25 +72,13 @@ export const SPACE_AGGREGATION_OPTIONS_FOR_EXPANDED_VIEW: Record<
};
export const INITIAL_INSPECT_METRICS_OPTIONS: MetricInspectionOptions = {
currentOptions: {
timeAggregationOption: undefined,
timeAggregationInterval: undefined,
spaceAggregationOption: undefined,
spaceAggregationLabels: [],
filters: {
items: [],
op: 'AND',
},
},
appliedOptions: {
timeAggregationOption: undefined,
timeAggregationInterval: undefined,
spaceAggregationOption: undefined,
spaceAggregationLabels: [],
filters: {
items: [],
op: 'AND',
},
timeAggregationOption: undefined,
timeAggregationInterval: undefined,
spaceAggregationOption: undefined,
spaceAggregationLabels: [],
filters: {
items: [],
op: 'AND',
},
};

View File

@@ -43,36 +43,36 @@ export interface GraphViewProps {
showExpandedView: boolean;
setShowExpandedView: (showExpandedView: boolean) => void;
setExpandedViewOptions: (options: GraphPopoverOptions | null) => void;
appliedMetricInspectionOptions: InspectOptions;
metricInspectionOptions: MetricInspectionOptions;
isInspectMetricsRefetching: boolean;
}
export interface QueryBuilderProps {
currentMetricName: string | null;
setCurrentMetricName: (metricName: string) => void;
setAppliedMetricName: (metricName: string) => void;
metricName: string | null;
setMetricName: (metricName: string) => void;
metricType: MetricType | undefined;
spaceAggregationLabels: string[];
currentMetricInspectionOptions: InspectOptions;
metricInspectionOptions: MetricInspectionOptions;
dispatchMetricInspectionOptions: (action: MetricInspectionAction) => void;
inspectionStep: InspectionStep;
inspectMetricsTimeSeries: InspectMetricsSeries[];
currentQuery: IBuilderQuery;
setCurrentQuery: (query: IBuilderQuery) => void;
searchQuery: IBuilderQuery;
}
export interface MetricNameSearchProps {
currentMetricName: string | null;
setCurrentMetricName: (metricName: string) => void;
metricName: string | null;
setMetricName: (metricName: string) => void;
}
export interface MetricFiltersProps {
searchQuery: IBuilderQuery;
dispatchMetricInspectionOptions: (action: MetricInspectionAction) => void;
currentQuery: IBuilderQuery;
setCurrentQuery: (query: IBuilderQuery) => void;
metricName: string | null;
metricType: MetricType | null;
}
export interface MetricTimeAggregationProps {
currentMetricInspectionOptions: InspectOptions;
metricInspectionOptions: MetricInspectionOptions;
dispatchMetricInspectionOptions: (action: MetricInspectionAction) => void;
inspectionStep: InspectionStep;
inspectMetricsTimeSeries: InspectMetricsSeries[];
@@ -80,7 +80,7 @@ export interface MetricTimeAggregationProps {
export interface MetricSpaceAggregationProps {
spaceAggregationLabels: string[];
currentMetricInspectionOptions: InspectOptions;
metricInspectionOptions: MetricInspectionOptions;
dispatchMetricInspectionOptions: (action: MetricInspectionAction) => void;
inspectionStep: InspectionStep;
}
@@ -101,7 +101,7 @@ export enum SpaceAggregationOptions {
AVG_BY = 'avg_by',
}
export interface InspectOptions {
export interface MetricInspectionOptions {
timeAggregationOption: TimeAggregationOptions | undefined;
timeAggregationInterval: number | undefined;
spaceAggregationOption: SpaceAggregationOptions | undefined;
@@ -109,19 +109,13 @@ export interface InspectOptions {
filters: TagFilter;
}
export interface MetricInspectionOptions {
currentOptions: InspectOptions;
appliedOptions: InspectOptions;
}
export type MetricInspectionAction =
| { type: 'SET_TIME_AGGREGATION_OPTION'; payload: TimeAggregationOptions }
| { type: 'SET_TIME_AGGREGATION_INTERVAL'; payload: number }
| { type: 'SET_SPACE_AGGREGATION_OPTION'; payload: SpaceAggregationOptions }
| { type: 'SET_SPACE_AGGREGATION_LABELS'; payload: string[] }
| { type: 'SET_FILTERS'; payload: TagFilter }
| { type: 'RESET_INSPECTION' }
| { type: 'APPLY_INSPECTION_OPTIONS' };
| { type: 'RESET_INSPECTION' };
export enum InspectionStep {
TIME_AGGREGATION = 1,
@@ -162,7 +156,7 @@ export interface ExpandedViewProps {
options: GraphPopoverOptions | null;
spaceAggregationSeriesMap: Map<string, InspectMetricsSeries[]>;
step: InspectionStep;
appliedMetricInspectionOptions: InspectOptions;
metricInspectionOptions: MetricInspectionOptions;
timeAggregatedSeriesMap: Map<number, GraphPopoverData[]>;
}
@@ -171,7 +165,7 @@ export interface TableViewProps {
inspectMetricsTimeSeries: InspectMetricsSeries[];
setShowExpandedView: (showExpandedView: boolean) => void;
setExpandedViewOptions: (options: GraphPopoverOptions | null) => void;
appliedMetricInspectionOptions: InspectOptions;
metricInspectionOptions: MetricInspectionOptions;
isInspectMetricsRefetching: boolean;
}

View File

@@ -27,55 +27,30 @@ const metricInspectionReducer = (
case 'SET_TIME_AGGREGATION_OPTION':
return {
...state,
currentOptions: {
...state.currentOptions,
timeAggregationOption: action.payload,
},
timeAggregationOption: action.payload,
};
case 'SET_TIME_AGGREGATION_INTERVAL':
return {
...state,
currentOptions: {
...state.currentOptions,
timeAggregationInterval: action.payload,
},
timeAggregationInterval: action.payload,
};
case 'SET_SPACE_AGGREGATION_OPTION':
return {
...state,
currentOptions: {
...state.currentOptions,
spaceAggregationOption: action.payload,
},
spaceAggregationOption: action.payload,
};
case 'SET_SPACE_AGGREGATION_LABELS':
return {
...state,
currentOptions: {
...state.currentOptions,
spaceAggregationLabels: action.payload,
},
spaceAggregationLabels: action.payload,
};
case 'SET_FILTERS':
return {
...state,
currentOptions: {
...state.currentOptions,
filters: action.payload,
},
};
case 'APPLY_INSPECTION_OPTIONS':
return {
...state,
appliedOptions: {
...state.appliedOptions,
...state.currentOptions,
},
filters: action.payload,
};
case 'RESET_INSPECTION':
return {
...INITIAL_INSPECT_METRICS_OPTIONS,
};
return { ...INITIAL_INSPECT_METRICS_OPTIONS };
default:
return state;
}
@@ -109,7 +84,7 @@ export function useInspectMetrics(
metricName: metricName ?? '',
start,
end,
filters: metricInspectionOptions.appliedOptions.filters,
filters: metricInspectionOptions.filters,
},
{
enabled: !!metricName,
@@ -142,26 +117,13 @@ export function useInspectMetrics(
);
// Evaluate inspection step
const currentInspectionStep = useMemo(() => {
if (metricInspectionOptions.currentOptions.spaceAggregationOption) {
const inspectionStep = useMemo(() => {
if (metricInspectionOptions.spaceAggregationOption) {
return InspectionStep.COMPLETED;
}
if (
metricInspectionOptions.currentOptions.timeAggregationOption &&
metricInspectionOptions.currentOptions.timeAggregationInterval
) {
return InspectionStep.SPACE_AGGREGATION;
}
return InspectionStep.TIME_AGGREGATION;
}, [metricInspectionOptions]);
const appliedInspectionStep = useMemo(() => {
if (metricInspectionOptions.appliedOptions.spaceAggregationOption) {
return InspectionStep.COMPLETED;
}
if (
metricInspectionOptions.appliedOptions.timeAggregationOption &&
metricInspectionOptions.appliedOptions.timeAggregationInterval
metricInspectionOptions.timeAggregationOption &&
metricInspectionOptions.timeAggregationInterval
) {
return InspectionStep.SPACE_AGGREGATION;
}
@@ -187,26 +149,23 @@ export function useInspectMetrics(
// Apply time aggregation once required options are set
if (
appliedInspectionStep >= InspectionStep.SPACE_AGGREGATION &&
metricInspectionOptions.appliedOptions.timeAggregationOption &&
metricInspectionOptions.appliedOptions.timeAggregationInterval
inspectionStep >= InspectionStep.SPACE_AGGREGATION &&
metricInspectionOptions.timeAggregationOption &&
metricInspectionOptions.timeAggregationInterval
) {
const {
timeAggregatedSeries,
timeAggregatedSeriesMap,
} = applyTimeAggregation(
inspectMetricsTimeSeries,
metricInspectionOptions.appliedOptions,
);
} = applyTimeAggregation(inspectMetricsTimeSeries, metricInspectionOptions);
timeSeries = timeAggregatedSeries;
setTimeAggregatedSeriesMap(timeAggregatedSeriesMap);
setAggregatedTimeSeries(timeSeries);
}
// Apply space aggregation
if (appliedInspectionStep === InspectionStep.COMPLETED) {
if (inspectionStep === InspectionStep.COMPLETED) {
const { aggregatedSeries, spaceAggregatedSeriesMap } = applySpaceAggregation(
timeSeries,
metricInspectionOptions.appliedOptions,
metricInspectionOptions,
);
timeSeries = aggregatedSeries;
setSpaceAggregatedSeriesMap(spaceAggregatedSeriesMap);
@@ -227,7 +186,7 @@ export function useInspectMetrics(
const rawData = [timestamps, ...timeseriesArray];
return rawData.map((series) => new Float64Array(series));
}, [inspectMetricsTimeSeries, appliedInspectionStep, metricInspectionOptions]);
}, [inspectMetricsTimeSeries, inspectionStep, metricInspectionOptions]);
const spaceAggregationLabels = useMemo(() => {
const labels = new Set<string>();
@@ -257,7 +216,7 @@ export function useInspectMetrics(
spaceAggregationLabels,
metricInspectionOptions,
dispatchMetricInspectionOptions,
inspectionStep: currentInspectionStep,
inspectionStep,
isInspectMetricsRefetching,
spaceAggregatedSeriesMap,
aggregatedTimeSeries,

View File

@@ -4,12 +4,16 @@ import logEvent from 'api/common/logEvent';
import { InspectMetricsSeries } from 'api/metricsExplorer/getInspectMetricsDetails';
import { MetricType } from 'api/metricsExplorer/getMetricsList';
import classNames from 'classnames';
import QuerySearch from 'components/QueryBuilderV2/QueryV2/QuerySearch/QuerySearch';
import { convertExpressionToFilters } from 'components/QueryBuilderV2/utils';
import { initialQueriesMap } from 'constants/queryBuilder';
import { AggregatorFilter } from 'container/QueryBuilder/filters';
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import { HardHat } from 'lucide-react';
import { useMemo, useState } from 'react';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import {
BaseAutocompleteData,
DataTypes,
} from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
@@ -22,8 +26,8 @@ import {
GraphPopoverData,
GraphPopoverOptions,
InspectionStep,
InspectOptions,
MetricFiltersProps,
MetricInspectionOptions,
MetricNameSearchProps,
MetricSpaceAggregationProps,
MetricTimeAggregationProps,
@@ -67,13 +71,13 @@ export function getDefaultTimeAggregationInterval(
}
export function MetricNameSearch({
currentMetricName,
setCurrentMetricName,
metricName,
setMetricName,
}: MetricNameSearchProps): JSX.Element {
const [searchText, setSearchText] = useState(currentMetricName);
const [searchText, setSearchText] = useState(metricName);
const handleSetMetricName = (value: BaseAutocompleteData): void => {
setCurrentMetricName(value.key);
setMetricName(value.key);
};
const handleChange = (value: BaseAutocompleteData): void => {
@@ -98,31 +102,27 @@ export function MetricNameSearch({
export function MetricFilters({
dispatchMetricInspectionOptions,
currentQuery,
setCurrentQuery,
searchQuery,
metricName,
metricType,
}: MetricFiltersProps): JSX.Element {
const handleOnChange = (expression: string): void => {
logEvent(MetricsExplorerEvents.FilterApplied, {
[MetricsExplorerEventKeys.Modal]: 'inspect',
});
const tagFilter = {
items: convertExpressionToFilters(expression),
op: 'AND',
};
setCurrentQuery({
...currentQuery,
filters: tagFilter,
filter: {
...currentQuery.filter,
expression,
},
expression,
});
dispatchMetricInspectionOptions({
type: 'SET_FILTERS',
payload: tagFilter,
});
};
const { handleChangeQueryData } = useQueryOperations({
index: 0,
query: searchQuery,
entityVersion: '',
});
const aggregateAttribute = useMemo(
() => ({
key: metricName ?? '',
dataType: DataTypes.String,
type: metricType,
isColumn: true,
isJSON: false,
id: `${metricName}--${DataTypes.String}--${metricType}--true`,
}),
[metricName, metricType],
);
return (
<div
@@ -130,19 +130,30 @@ export function MetricFilters({
className="inspect-metrics-input-group metric-filters"
>
<Typography.Text>Where</Typography.Text>
{currentQuery && (
<QuerySearch
queryData={currentQuery}
onChange={handleOnChange}
dataSource={DataSource.METRICS}
/>
)}
<QueryBuilderSearch
query={{
...searchQuery,
aggregateAttribute,
}}
onChange={(value): void => {
handleChangeQueryData('filters', value);
logEvent(MetricsExplorerEvents.FilterApplied, {
[MetricsExplorerEventKeys.Modal]: 'inspect',
});
dispatchMetricInspectionOptions({
type: 'SET_FILTERS',
payload: value,
});
}}
suffixIcon={<HardHat size={16} />}
disableNavigationShortcuts
/>
</div>
);
}
export function MetricTimeAggregation({
currentMetricInspectionOptions,
metricInspectionOptions,
dispatchMetricInspectionOptions,
inspectionStep,
inspectMetricsTimeSeries,
@@ -163,14 +174,14 @@ export function MetricTimeAggregation({
<div className="inspect-metrics-input-group">
<Typography.Text>Align with</Typography.Text>
<Select
value={currentMetricInspectionOptions.timeAggregationOption}
value={metricInspectionOptions.timeAggregationOption}
onChange={(value): void => {
dispatchMetricInspectionOptions({
type: 'SET_TIME_AGGREGATION_OPTION',
payload: value,
});
// set the time aggregation interval to the default value if it is not set
if (!currentMetricInspectionOptions.timeAggregationInterval) {
if (!metricInspectionOptions.timeAggregationInterval) {
dispatchMetricInspectionOptions({
type: 'SET_TIME_AGGREGATION_INTERVAL',
payload: getDefaultTimeAggregationInterval(
@@ -194,7 +205,7 @@ export function MetricTimeAggregation({
<Input
type="number"
className="no-arrows-input"
value={currentMetricInspectionOptions.timeAggregationInterval}
value={metricInspectionOptions.timeAggregationInterval}
placeholder="Select interval..."
suffix="seconds"
onChange={(e): void => {
@@ -213,7 +224,7 @@ export function MetricTimeAggregation({
export function MetricSpaceAggregation({
spaceAggregationLabels,
currentMetricInspectionOptions,
metricInspectionOptions,
dispatchMetricInspectionOptions,
inspectionStep,
}: MetricSpaceAggregationProps): JSX.Element {
@@ -232,7 +243,7 @@ export function MetricSpaceAggregation({
<div className="metric-space-aggregation-content">
<div className="metric-space-aggregation-content-left">
<Select
value={currentMetricInspectionOptions.spaceAggregationOption}
value={metricInspectionOptions.spaceAggregationOption}
placeholder="Select option"
onChange={(value): void => {
dispatchMetricInspectionOptions({
@@ -255,7 +266,7 @@ export function MetricSpaceAggregation({
mode="multiple"
style={{ width: '100%' }}
placeholder="Search for attributes..."
value={currentMetricInspectionOptions.spaceAggregationLabels}
value={metricInspectionOptions.spaceAggregationLabels}
onChange={(value): void => {
dispatchMetricInspectionOptions({
type: 'SET_SPACE_AGGREGATION_LABELS',
@@ -311,7 +322,7 @@ export function applyFilters(
export function applyTimeAggregation(
inspectMetricsTimeSeries: InspectMetricsSeries[],
appliedMetricInspectionOptions: InspectOptions,
metricInspectionOptions: MetricInspectionOptions,
): {
timeAggregatedSeries: InspectMetricsSeries[];
timeAggregatedSeriesMap: Map<number, GraphPopoverData[]>;
@@ -319,7 +330,7 @@ export function applyTimeAggregation(
const {
timeAggregationOption,
timeAggregationInterval,
} = appliedMetricInspectionOptions;
} = metricInspectionOptions;
if (!timeAggregationInterval) {
return {
@@ -404,7 +415,7 @@ export function applyTimeAggregation(
export function applySpaceAggregation(
inspectMetricsTimeSeries: InspectMetricsSeries[],
appliedMetricInspectionOptions: InspectOptions,
metricInspectionOptions: MetricInspectionOptions,
): {
aggregatedSeries: InspectMetricsSeries[];
spaceAggregatedSeriesMap: Map<string, InspectMetricsSeries[]>;
@@ -414,7 +425,7 @@ export function applySpaceAggregation(
inspectMetricsTimeSeries.forEach((series) => {
// Create composite key from selected labels
const key = appliedMetricInspectionOptions.spaceAggregationLabels
const key = metricInspectionOptions.spaceAggregationLabels
.map((label) => `${label}:${series.labels[label]}`)
.join(',');
@@ -449,7 +460,7 @@ export function applySpaceAggregation(
([timestamp, values]) => {
let aggregatedValue: number;
switch (appliedMetricInspectionOptions.spaceAggregationOption) {
switch (metricInspectionOptions.spaceAggregationOption) {
case SpaceAggregationOptions.SUM_BY:
aggregatedValue = values.reduce((sum, val) => sum + val, 0);
break;
@@ -703,11 +714,11 @@ export function getTimeSeriesLabel(
export function HoverPopover({
options,
step,
appliedMetricInspectionOptions,
metricInspectionOptions,
}: {
options: GraphPopoverOptions;
step: InspectionStep;
appliedMetricInspectionOptions: InspectOptions;
metricInspectionOptions: MetricInspectionOptions;
}): JSX.Element {
const closestTimestamp = useMemo(() => {
if (!options.timeSeries) {
@@ -735,7 +746,7 @@ export function HoverPopover({
const title = useMemo(() => {
if (
step === InspectionStep.COMPLETED &&
appliedMetricInspectionOptions.spaceAggregationLabels.length === 0
metricInspectionOptions.spaceAggregationLabels.length === 0
) {
return undefined;
}
@@ -749,7 +760,7 @@ export function HoverPopover({
options.timeSeries,
options.timeSeries?.strokeColor,
);
}, [step, options.timeSeries, appliedMetricInspectionOptions]);
}, [step, options.timeSeries, metricInspectionOptions]);
return (
<Card
@@ -819,26 +830,3 @@ export function onGraphHover(
timeSeries: series,
});
}
export function useMetricName(
metricName: string | null,
): {
currentMetricName: string | null;
setCurrentMetricName: (metricName: string | null) => void;
appliedMetricName: string | null;
setAppliedMetricName: (metricName: string | null) => void;
} {
const [currentMetricName, setCurrentMetricName] = useState<string | null>(
metricName,
);
const [appliedMetricName, setAppliedMetricName] = useState<string | null>(
metricName,
);
return {
currentMetricName,
setCurrentMetricName,
appliedMetricName,
setAppliedMetricName,
};
}

View File

@@ -1,97 +1,27 @@
import { Button, Tooltip } from 'antd';
import QuerySearch from 'components/QueryBuilderV2/QueryV2/QuerySearch/QuerySearch';
import { convertExpressionToFilters } from 'components/QueryBuilderV2/utils';
import { Tooltip } from 'antd';
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import { cloneDeep } from 'lodash-es';
import { Info, Play } from 'lucide-react';
import { useEffect, useState } from 'react';
import {
IBuilderQuery,
TagFilter,
} from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { HardHat, Info } from 'lucide-react';
import { MetricsSearchProps } from './types';
function MetricsSearch({ onChange, query }: MetricsSearchProps): JSX.Element {
const [contextQuery, setContextQuery] = useState<IBuilderQuery | undefined>(
query,
);
useEffect(() => {
setContextQuery(query);
}, [query]);
const handleRunQuery = (expression: string): void => {
let updatedContextQuery = cloneDeep(contextQuery);
if (!updatedContextQuery) {
return;
}
const newFilters: TagFilter = {
items: expression ? convertExpressionToFilters(expression) : [],
op: 'AND',
};
updatedContextQuery = {
...updatedContextQuery,
filter: {
...updatedContextQuery.filter,
expression,
},
filters: {
...updatedContextQuery.filters,
...newFilters,
op: updatedContextQuery.filters?.op ?? 'AND',
},
};
setContextQuery(updatedContextQuery);
onChange(newFilters);
};
const handleOnChange = (expression: string): void => {
let updatedContextQuery = cloneDeep(contextQuery);
if (updatedContextQuery) {
updatedContextQuery = {
...updatedContextQuery,
filter: {
...updatedContextQuery.filter,
expression,
},
};
setContextQuery(updatedContextQuery);
}
};
const handleStageAndRunQuery = (): void =>
handleRunQuery(contextQuery?.filter?.expression || '');
function MetricsSearch({ query, onChange }: MetricsSearchProps): JSX.Element {
return (
<div className="metrics-search-container">
<div data-testid="qb-search-container" className="qb-search-container">
<div className="qb-search-container">
<Tooltip
title="Use filters to refine metrics based on attributes. Example: service_name=api - Shows all metrics associated with the API service"
placement="right"
>
<Info size={16} />
</Tooltip>
{contextQuery && (
<QuerySearch
onChange={handleOnChange}
dataSource={DataSource.METRICS}
queryData={contextQuery}
onRun={handleRunQuery}
isMetricsExplorer
/>
)}
<QueryBuilderSearch
query={query}
onChange={onChange}
suffixIcon={<HardHat size={16} />}
isMetricsExplorer
/>
</div>
<Button
type="primary"
onClick={handleStageAndRunQuery}
className="stage-run-query"
icon={<Play size={14} />}
>
Stage & Run Query
</Button>
<div className="metrics-search-options">
<DateTimeSelectionV2
showAutoRefresh={false}

View File

@@ -37,7 +37,6 @@
.metrics-search-container {
display: flex;
align-items: center;
gap: 16px;
.metrics-search-options {

View File

@@ -3,7 +3,6 @@ import './Summary.styles.scss';
import * as Sentry from '@sentry/react';
import logEvent from 'api/common/logEvent';
import { convertFiltersToExpression } from 'components/QueryBuilderV2/utils';
import { initialQueriesMap } from 'constants/queryBuilder';
import { usePageSize } from 'container/InfraMonitoringK8s/utils';
import NoLogs from 'container/NoLogs/NoLogs';
@@ -185,7 +184,6 @@ function Summary(): JSX.Element {
() => ({
...initialQueriesMap.metrics.builder.queryData[0],
filters: queryFilters,
filter: convertFiltersToExpression(queryFilters),
}),
[queryFilters],
);
@@ -292,11 +290,18 @@ function Summary(): JSX.Element {
],
);
console.log({
isMetricsListDataEmpty,
isMetricsTreeMapDataEmpty,
treeMapData,
sec: treeMapData?.payload?.data[heatmapView],
});
return (
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
<div className="metrics-explorer-summary-tab">
<MetricsSearch query={searchQuery} onChange={handleFilterChange} />
{isMetricsLoading && isTreeMapLoading ? (
{isMetricsLoading || isTreeMapLoading ? (
<MetricsLoading />
) : isMetricsListDataEmpty && isMetricsTreeMapDataEmpty ? (
<NoLogs dataSource={DataSource.METRICS} />

View File

@@ -1,35 +0,0 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { initialQueriesMap } from 'constants/queryBuilder';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import MetricsSearch from '../MetricsSearch';
jest.mock('container/TopNav/DateTimeSelectionV2', () => ({
__esModule: true,
default: (): JSX.Element => (
<div data-testid="date-time-selection">DateTime</div>
),
}));
const mockQuery: IBuilderQuery = {
...initialQueriesMap.metrics.builder.queryData[0],
};
const mockOnChange = jest.fn();
describe('MetricsSearch', () => {
it('should render the search bar, run button and date-time selector', () => {
render(<MetricsSearch query={mockQuery} onChange={mockOnChange} />);
expect(screen.getByText('DateTime')).toBeInTheDocument();
expect(screen.getByText('Stage & Run Query')).toBeInTheDocument();
expect(screen.getByTestId('qb-search-container')).toBeInTheDocument();
});
it('should call onChange with parsed filters when Stage & Run is clicked and expression is present', () => {
render(<MetricsSearch query={mockQuery} onChange={mockOnChange} />);
fireEvent.click(screen.getByText('Stage & Run Query'));
expect(mockOnChange).toHaveBeenCalledWith({
items: [],
op: 'AND',
});
});
});

View File

@@ -30,5 +30,14 @@ 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,6 +39,7 @@ interface QBEntityOptionsProps {
showCloneOption?: boolean;
isListViewPanel?: boolean;
index?: number;
hasTraceOperator?: boolean;
queryVariant?: 'dropdown' | 'static';
onChangeDataSource?: (value: DataSource) => void;
}
@@ -61,6 +62,7 @@ export default function QBEntityOptions({
onCloneQuery,
index,
queryVariant,
hasTraceOperator = false,
onChangeDataSource,
}: QBEntityOptionsProps): JSX.Element {
const handleCloneEntity = (): void => {
@@ -97,7 +99,7 @@ export default function QBEntityOptions({
value="query-builder"
className="periscope-btn visibility-toggle"
onClick={onToggleVisibility}
disabled={isListViewPanel}
disabled={isListViewPanel && query?.dataSource !== DataSource.TRACES}
>
{entityData.disabled ? <EyeOff size={16} /> : <Eye size={16} />}
</Button>
@@ -115,6 +117,7 @@ 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,5 +11,7 @@ export type QueryProps = {
version: string;
showSpanScopeSelector?: boolean;
showOnlyWhereClause?: boolean;
showTraceOperator?: boolean;
signalSource?: string;
isMultiQueryAllowed?: boolean;
} & Pick<QueryBuilderProps, 'filterConfigs' | 'queryComponents'>;

View File

@@ -0,0 +1,160 @@
import { Button, Popover, Spin, Tooltip } from 'antd';
import GroupByIcon from 'assets/CustomIcons/GroupByIcon';
import { OPERATORS } from 'constants/antlrQueryConstants';
import { useTraceActions } from 'hooks/trace/useTraceActions';
import { ArrowDownToDot, ArrowUpFromDot, Copy, Ellipsis } from 'lucide-react';
import { useCallback, useMemo, useState } from 'react';
interface AttributeRecord {
field: string;
value: string;
}
interface AttributeActionsProps {
record: AttributeRecord;
}
export default function AttributeActions({
record,
}: AttributeActionsProps): JSX.Element {
const [isOpen, setIsOpen] = useState<boolean>(false);
const [isFilterInLoading, setIsFilterInLoading] = useState<boolean>(false);
const [isFilterOutLoading, setIsFilterOutLoading] = useState<boolean>(false);
const {
onAddToQuery,
onGroupByAttribute,
onCopyFieldName,
onCopyFieldValue,
} = useTraceActions();
const textToCopy = useMemo(() => {
const str = record.value == null ? '' : String(record.value);
// Remove surrounding double-quotes only (e.g., JSON-encoded string values)
return str.replace(/^"|"$/g, '');
}, [record.value]);
const handleFilterIn = useCallback(async (): Promise<void> => {
if (!onAddToQuery || isFilterInLoading) return;
setIsFilterInLoading(true);
try {
await Promise.resolve(
onAddToQuery(record.field, record.value, OPERATORS['=']),
);
} finally {
setIsFilterInLoading(false);
}
}, [onAddToQuery, record.field, record.value, isFilterInLoading]);
const handleFilterOut = useCallback(async (): Promise<void> => {
if (!onAddToQuery || isFilterOutLoading) return;
setIsFilterOutLoading(true);
try {
await Promise.resolve(
onAddToQuery(record.field, record.value, OPERATORS['!=']),
);
} finally {
setIsFilterOutLoading(false);
}
}, [onAddToQuery, record.field, record.value, isFilterOutLoading]);
const handleGroupBy = useCallback((): void => {
if (onGroupByAttribute) {
onGroupByAttribute(record.field);
}
setIsOpen(false);
}, [onGroupByAttribute, record.field]);
const handleCopyFieldName = useCallback((): void => {
if (onCopyFieldName) {
onCopyFieldName(record.field);
}
setIsOpen(false);
}, [onCopyFieldName, record.field]);
const handleCopyFieldValue = useCallback((): void => {
if (onCopyFieldValue) {
onCopyFieldValue(textToCopy);
}
setIsOpen(false);
}, [onCopyFieldValue, textToCopy]);
const moreActionsContent = (
<div className="attribute-actions-menu">
<Button
className="group-by-clause"
type="text"
icon={<GroupByIcon />}
onClick={handleGroupBy}
block
>
Group By Attribute
</Button>
<Button
type="text"
icon={<Copy size={14} />}
onClick={handleCopyFieldName}
block
>
Copy Field Name
</Button>
<Button
type="text"
icon={<Copy size={14} />}
onClick={handleCopyFieldValue}
block
>
Copy Field Value
</Button>
</div>
);
return (
<div className="action-btn">
<Tooltip title="Filter for value">
<Button
className="filter-btn periscope-btn"
aria-label="Filter for value"
disabled={isFilterInLoading}
icon={
isFilterInLoading ? (
<Spin size="small" />
) : (
<ArrowDownToDot size={14} style={{ transform: 'rotate(90deg)' }} />
)
}
onClick={handleFilterIn}
/>
</Tooltip>
<Tooltip title="Filter out value">
<Button
className="filter-btn periscope-btn"
aria-label="Filter out value"
disabled={isFilterOutLoading}
icon={
isFilterOutLoading ? (
<Spin size="small" />
) : (
<ArrowUpFromDot size={14} style={{ transform: 'rotate(90deg)' }} />
)
}
onClick={handleFilterOut}
/>
</Tooltip>
<Popover
open={isOpen}
onOpenChange={setIsOpen}
arrow={false}
content={moreActionsContent}
rootClassName="attribute-actions-content"
trigger="hover"
placement="bottomLeft"
>
<Button
icon={<Ellipsis size={14} />}
className="filter-btn periscope-btn"
/>
</Popover>
</div>
);
}

View File

@@ -24,6 +24,13 @@
flex-direction: column;
gap: 8px;
justify-content: flex-start;
position: relative;
&:hover {
.action-btn {
display: flex;
}
}
.item-key {
color: var(--bg-vanilla-100);
@@ -40,11 +47,12 @@
padding: 2px 8px;
align-items: center;
width: fit-content;
max-width: 100%;
max-width: calc(100% - 120px); /* Reserve space for action buttons */
gap: 8px;
border-radius: 50px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-slate-500);
.item-value {
color: var(--bg-vanilla-400);
font-family: Inter;
@@ -55,6 +63,35 @@
letter-spacing: 0.56px;
}
}
.action-btn {
display: none;
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
gap: 4px;
background: rgba(0, 0, 0, 0.8);
border-radius: 4px;
padding: 2px;
.filter-btn {
display: flex;
align-items: center;
border: none;
box-shadow: none;
border-radius: 2px;
background: var(--bg-slate-400);
padding: 4px;
gap: 3px;
height: 24px;
width: 24px;
&:hover {
background: var(--bg-slate-300);
}
}
}
}
}
@@ -63,6 +100,36 @@
}
}
.attribute-actions-menu {
display: flex;
flex-direction: column;
gap: 4px;
.ant-btn {
text-align: left;
height: auto;
padding: 6px 12px;
display: flex;
align-items: center;
gap: 8px;
&:hover {
background-color: var(--bg-slate-400);
}
}
.group-by-clause {
color: var(--text-primary);
}
}
.attribute-actions-content {
.ant-popover-inner {
padding: 8px;
min-width: 160px;
}
}
.lightMode {
.attributes-corner {
.attributes-container {
@@ -79,6 +146,18 @@
color: var(--bg-ink-400);
}
}
.action-btn {
background: rgba(255, 255, 255, 0.9);
.filter-btn {
background: var(--bg-vanilla-200);
&:hover {
background: var(--bg-vanilla-100);
}
}
}
}
}
@@ -86,4 +165,12 @@
border-top: 1px solid var(--bg-vanilla-300);
}
}
.attribute-actions-menu {
.ant-btn {
&:hover {
background-color: var(--bg-vanilla-200);
}
}
}
}

View File

@@ -2,11 +2,13 @@ import './Attributes.styles.scss';
import { Input, Tooltip, Typography } from 'antd';
import cx from 'classnames';
import CopyClipboardHOC from 'components/Logs/CopyClipboardHOC';
import { flattenObject } from 'container/LogDetailedView/utils';
import { useMemo, useState } from 'react';
import { Span } from 'types/api/trace/getTraceV2';
import NoData from '../NoData/NoData';
import AttributeActions from './AttributeActions';
interface IAttributesProps {
span: Span;
@@ -53,10 +55,13 @@ function Attributes(props: IAttributesProps): JSX.Element {
</Typography.Text>
<div className="value-wrapper">
<Tooltip title={item.value}>
<Typography.Text className="item-value" ellipsis>
{item.value}
</Typography.Text>
<CopyClipboardHOC entityKey={item.value} textToCopy={item.value}>
<Typography.Text className="item-value" ellipsis>
{item.value}
</Typography.Text>
</CopyClipboardHOC>
</Tooltip>
<AttributeActions record={item} />
</div>
</div>
))}

View File

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

View File

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

View File

@@ -0,0 +1,193 @@
import { getAggregateKeys } from 'api/queryBuilder/getAttributeKeys';
import { convertFiltersToExpressionWithExistingQuery } from 'components/QueryBuilderV2/utils';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { QueryBuilderKeys } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useNotifications } from 'hooks/useNotifications';
import { chooseAutocompleteFromCustomValue } from 'lib/newQueryBuilder/chooseAutocompleteFromCustomValue';
import { useCallback } from 'react';
import { useQueryClient } from 'react-query';
import { useCopyToClipboard } from 'react-use';
import {
BaseAutocompleteData,
DataTypes,
} from 'types/api/queryBuilder/queryAutocompleteResponse';
import { Query, TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { v4 as uuid } from 'uuid';
export interface UseTraceActionsReturn {
onAddToQuery: (
fieldKey: string,
fieldValue: string,
operator: string,
) => Promise<void>;
onGroupByAttribute: (fieldKey: string) => Promise<void>;
onCopyFieldName: (fieldName: string) => void;
onCopyFieldValue: (fieldValue: string) => void;
}
export const useTraceActions = (): UseTraceActionsReturn => {
const { currentQuery, redirectWithQueryBuilderData } = useQueryBuilder();
const queryClient = useQueryClient();
const { notifications } = useNotifications();
const [, setCopy] = useCopyToClipboard();
const removeExistingFieldFilters = useCallback(
(filters: TagFilterItem[], fieldKey: BaseAutocompleteData): TagFilterItem[] =>
filters.filter((filter: TagFilterItem) => filter.key?.key !== fieldKey.key),
[],
);
const getAutocompleteKey = useCallback(
async (fieldKey: string): Promise<BaseAutocompleteData> => {
const keysAutocompleteResponse = await queryClient.fetchQuery(
[QueryBuilderKeys.GET_AGGREGATE_KEYS, fieldKey],
async () =>
getAggregateKeys({
searchText: fieldKey,
aggregateOperator:
currentQuery.builder.queryData[0].aggregateOperator || '',
dataSource: DataSource.TRACES,
aggregateAttribute:
currentQuery.builder.queryData[0].aggregateAttribute?.key || '',
}),
);
const keysAutocomplete: BaseAutocompleteData[] =
keysAutocompleteResponse.payload?.attributeKeys || [];
return chooseAutocompleteFromCustomValue(
keysAutocomplete,
fieldKey,
false,
DataTypes.String,
);
},
[queryClient, currentQuery.builder.queryData],
);
const onAddToQuery = useCallback(
async (
fieldKey: string,
fieldValue: string,
operator: string,
): Promise<void> => {
try {
const existAutocompleteKey = await getAutocompleteKey(fieldKey);
const currentOperator = getOperatorValue(operator);
const nextQuery: Query = {
...currentQuery,
builder: {
...currentQuery.builder,
queryData: currentQuery.builder.queryData.map((item) => {
// Get existing filters and remove any for the same field
const currentFilters = item.filters?.items || [];
const cleanedFilters = removeExistingFieldFilters(
currentFilters,
existAutocompleteKey,
);
// Add the new filter to the cleaned list
const newFilters = [
...cleanedFilters,
{
id: uuid(),
key: existAutocompleteKey,
op: currentOperator,
value: fieldValue,
},
];
const convertedFilter = convertFiltersToExpressionWithExistingQuery(
{
items: newFilters,
op: item.filters?.op || 'AND',
},
item.filter?.expression || '',
);
return {
...item,
dataSource: DataSource.TRACES,
filters: convertedFilter.filters,
filter: convertedFilter.filter,
};
}),
},
};
redirectWithQueryBuilderData(nextQuery, {}, ROUTES.TRACES_EXPLORER);
} catch {
notifications.error({ message: SOMETHING_WENT_WRONG });
}
},
[
currentQuery,
notifications,
getAutocompleteKey,
redirectWithQueryBuilderData,
removeExistingFieldFilters,
],
);
const onGroupByAttribute = useCallback(
async (fieldKey: string): Promise<void> => {
try {
const existAutocompleteKey = await getAutocompleteKey(fieldKey);
const nextQuery: Query = {
...currentQuery,
builder: {
...currentQuery.builder,
queryData: currentQuery.builder.queryData.map((item) => ({
...item,
dataSource: DataSource.TRACES,
groupBy: [...item.groupBy, existAutocompleteKey],
})),
},
};
redirectWithQueryBuilderData(nextQuery, {}, ROUTES.TRACES_EXPLORER);
} catch {
notifications.error({ message: SOMETHING_WENT_WRONG });
}
},
[
currentQuery,
notifications,
getAutocompleteKey,
redirectWithQueryBuilderData,
],
);
const onCopyFieldName = useCallback(
(fieldName: string): void => {
setCopy(fieldName);
notifications.success({
message: 'Field name copied to clipboard',
});
},
[setCopy, notifications],
);
const onCopyFieldValue = useCallback(
(fieldValue: string): void => {
setCopy(fieldValue);
notifications.success({
message: 'Field value copied to clipboard',
});
},
[setCopy, notifications],
);
return {
onAddToQuery,
onGroupByAttribute,
onCopyFieldName,
onCopyFieldValue,
};
};

View File

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

View File

@@ -7,6 +7,7 @@ import {
initialClickHouseData,
initialFormulaBuilderFormValues,
initialQueriesMap,
initialQueryBuilderFormTraceOperatorValues,
initialQueryBuilderFormValuesMap,
initialQueryPromQLData,
initialQueryState,
@@ -14,6 +15,7 @@ import {
MAX_FORMULAS,
MAX_QUERIES,
PANEL_TYPES,
TRACE_OPERATOR_QUERY_NAME,
} from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import {
@@ -47,6 +49,7 @@ import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteRe
import {
IBuilderFormula,
IBuilderQuery,
IBuilderTraceOperator,
IClickHouseQuery,
IPromQLQuery,
Query,
@@ -61,6 +64,7 @@ import {
QueryBuilderData,
} from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { sanitizeOrderByForExplorer } from 'utils/sanitizeOrderBy';
import { v4 as uuid } from 'uuid';
export const QueryBuilderContext = createContext<QueryBuilderContextType>({
@@ -74,14 +78,18 @@ 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: () => {},
@@ -102,6 +110,12 @@ export function QueryBuilderProvider({
const currentPathnameRef = useRef<string | null>(location.pathname);
// This is used to determine if the query was called from the handleRunQuery function - which means manual trigger from Stage and Run button
const [
calledFromHandleRunQuery,
setCalledFromHandleRunQuery,
] = useState<boolean>(false);
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
@@ -166,6 +180,10 @@ export function QueryBuilderProvider({
...initialFormulaBuilderFormValues,
...item,
})),
queryTraceOperator: query.builder.queryTraceOperator?.map((item) => ({
...initialQueryBuilderFormTraceOperatorValues,
...item,
})),
};
const setupedQueryData = builder.queryData.map((item) => {
@@ -184,6 +202,17 @@ export function QueryBuilderProvider({
} as BaseAutocompleteData,
};
// Explorer pages: sanitize stale orderBy before first query
const isExplorer =
location.pathname === ROUTES.LOGS_EXPLORER ||
location.pathname === ROUTES.TRACES_EXPLORER;
if (isExplorer) {
const sanitizedOrderBy = sanitizeOrderByForExplorer(currentElement);
return calledFromHandleRunQuery
? currentElement
: { ...currentElement, orderBy: sanitizedOrderBy };
}
return currentElement;
});
@@ -215,7 +244,7 @@ export function QueryBuilderProvider({
return nextQuery;
},
[initialDataSource],
[initialDataSource, location.pathname, calledFromHandleRunQuery],
);
const initQueryBuilderData = useCallback(
@@ -367,8 +396,11 @@ export function QueryBuilderProvider({
const removeQueryBuilderEntityByIndex = useCallback(
(type: keyof QueryBuilderData, index: number) => {
setCurrentQuery((prevState) => {
const currentArray: (IBuilderQuery | IBuilderFormula)[] =
prevState.builder[type];
const currentArray: (
| IBuilderQuery
| IBuilderFormula
| IBuilderTraceOperator
)[] = prevState.builder[type];
const filteredArray = currentArray.filter((_, i) => index !== i);
@@ -382,8 +414,11 @@ export function QueryBuilderProvider({
});
// eslint-disable-next-line sonarjs/no-identical-functions
setSupersetQuery((prevState) => {
const currentArray: (IBuilderQuery | IBuilderFormula)[] =
prevState.builder[type];
const currentArray: (
| IBuilderQuery
| IBuilderFormula
| IBuilderTraceOperator
)[] = prevState.builder[type];
const filteredArray = currentArray.filter((_, i) => index !== i);
@@ -399,6 +434,20 @@ export function QueryBuilderProvider({
[],
);
const removeAllQueryBuilderEntities = useCallback(
(type: keyof QueryBuilderData) => {
setCurrentQuery((prevState) => ({
...prevState,
builder: { ...prevState.builder, [type]: [] },
}));
setSupersetQuery((prevState) => ({
...prevState,
builder: { ...prevState.builder, [type]: [] },
}));
},
[setCurrentQuery, setSupersetQuery],
);
const removeQueryTypeItemByIndex = useCallback(
(type: EQueryType.PROM | EQueryType.CLICKHOUSE, index: number) => {
setCurrentQuery((prevState) => {
@@ -428,6 +477,7 @@ export function QueryBuilderProvider({
const newQuery: IBuilderQuery = {
...initialBuilderQuery,
source: queries?.[0]?.source || '',
queryName: createNewBuilderItemName({ existNames, sourceNames: alphabet }),
expression: createNewBuilderItemName({
existNames,
@@ -522,6 +572,8 @@ export function QueryBuilderProvider({
setCurrentQuery((prevState) => {
if (prevState.builder.queryData.length >= MAX_QUERIES) return prevState;
console.log('prevState', prevState.builder.queryData);
const newQuery = createNewBuilderQuery(prevState.builder.queryData);
return {
@@ -532,6 +584,7 @@ export function QueryBuilderProvider({
},
};
});
// eslint-disable-next-line sonarjs/no-identical-functions
setSupersetQuery((prevState) => {
if (prevState.builder.queryData.length >= MAX_QUERIES) return prevState;
@@ -617,6 +670,68 @@ 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,
@@ -723,6 +838,44 @@ 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) => {
@@ -867,6 +1020,12 @@ export function QueryBuilderProvider({
const handleRunQuery = useCallback(
(shallUpdateStepInterval?: boolean, newQBQuery?: boolean) => {
const isExplorer =
location.pathname === ROUTES.LOGS_EXPLORER ||
location.pathname === ROUTES.TRACES_EXPLORER;
if (isExplorer) {
setCalledFromHandleRunQuery(true);
}
let currentQueryData = currentQuery;
if (newQBQuery) {
@@ -911,7 +1070,14 @@ export function QueryBuilderProvider({
queryType,
});
},
[currentQuery, queryType, maxTime, minTime, redirectWithQueryBuilderData],
[
location.pathname,
currentQuery,
queryType,
maxTime,
minTime,
redirectWithQueryBuilderData,
],
);
useEffect(() => {
@@ -921,6 +1087,7 @@ export function QueryBuilderProvider({
setStagedQuery(null);
// reset the last used query to 0 when navigating away from the page
setLastUsedQuery(0);
setCalledFromHandleRunQuery(false);
}
}, [location.pathname]);
@@ -1009,14 +1176,18 @@ export function QueryBuilderProvider({
panelType,
isEnabledQuery,
handleSetQueryData,
handleSetTraceOperatorData,
handleSetFormulaData,
handleSetQueryItemData,
handleSetConfig,
removeQueryBuilderEntityByIndex,
removeQueryTypeItemByIndex,
removeAllQueryBuilderEntities,
cloneQuery,
addNewBuilderQuery,
addNewFormula,
addTraceOperator,
removeTraceOperator,
addNewQueryItem,
redirectWithQueryBuilderData,
handleRunQuery,
@@ -1037,14 +1208,18 @@ export function QueryBuilderProvider({
panelType,
isEnabledQuery,
handleSetQueryData,
handleSetTraceOperatorData,
handleSetFormulaData,
handleSetQueryItemData,
handleSetConfig,
removeQueryBuilderEntityByIndex,
removeQueryTypeItemByIndex,
removeAllQueryBuilderEntities,
cloneQuery,
addNewBuilderQuery,
addNewFormula,
addTraceOperator,
removeTraceOperator,
addNewQueryItem,
redirectWithQueryBuilderData,
handleRunQuery,

View File

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

View File

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

View File

@@ -4,6 +4,7 @@ import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteRe
import {
IBuilderFormula,
IBuilderQuery,
IBuilderTraceOperator,
} from 'types/api/queryBuilder/queryBuilderData';
import {
BaseBuilderQuery,
@@ -18,6 +19,7 @@ import { SelectOption } from './select';
type UseQueryOperationsParams = Pick<QueryProps, 'index' | 'query'> &
Pick<QueryBuilderProps, 'filterConfigs'> & {
isForTraceOperator?: boolean;
formula?: IBuilderFormula;
isListViewPanel?: boolean;
entityVersion: string;
@@ -32,6 +34,14 @@ 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,6 +6,7 @@ import { Dispatch, SetStateAction } from 'react';
import {
IBuilderFormula,
IBuilderQuery,
IBuilderTraceOperator,
IClickHouseQuery,
IPromQLQuery,
Query,
@@ -222,6 +223,7 @@ export type ReduceOperators = 'last' | 'sum' | 'avg' | 'max' | 'min';
export type QueryBuilderData = {
queryData: IBuilderQuery[];
queryFormulas: IBuilderFormula[];
queryTraceOperator: IBuilderTraceOperator[];
};
export type QueryBuilderContextType = {
@@ -235,6 +237,10 @@ 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,
@@ -249,12 +255,15 @@ 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

@@ -0,0 +1,130 @@
/* eslint-disable sonarjs/no-duplicate-string */
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import {
IBuilderQuery,
OrderByPayload,
} from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { getParsedAggregationOptionsForOrderBy } from 'utils/aggregationConverter';
import { sanitizeOrderByForExplorer } from 'utils/sanitizeOrderBy';
jest.mock('utils/aggregationConverter', () => ({
getParsedAggregationOptionsForOrderBy: jest.fn(),
}));
const buildQuery = (overrides: Partial<IBuilderQuery> = {}): IBuilderQuery => ({
queryName: 'A',
dataSource: DataSource.TRACES,
aggregateOperator: '',
aggregateAttribute: undefined,
aggregations: [],
timeAggregation: '',
spaceAggregation: '',
temporality: '',
functions: [],
filter: { expression: '' } as any,
filters: { items: [], op: 'AND' } as any,
groupBy: [],
expression: '',
disabled: false,
having: [] as any,
limit: null,
stepInterval: 60 as any,
orderBy: [],
legend: '',
...overrides,
});
describe('sanitizeOrderByForExplorer', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('keeps only orderBy items that are present in groupBy keys or aggregation keys (including alias)', () => {
(getParsedAggregationOptionsForOrderBy as jest.Mock).mockReturnValue([
{
key: 'count()',
dataType: DataTypes.Float64,
isColumn: false,
type: '',
isJSON: false,
},
{
key: 'avg(duration)',
dataType: DataTypes.Float64,
isColumn: false,
type: '',
isJSON: false,
},
{
key: 'latency',
dataType: DataTypes.Float64,
isColumn: false,
type: '',
isJSON: false,
},
]);
const orderBy: OrderByPayload[] = [
{ columnName: 'service.name', order: 'asc' },
{ columnName: 'count()', order: 'desc' },
{ columnName: 'avg(duration)', order: 'asc' },
{ columnName: 'latency', order: 'asc' }, // alias
{ columnName: 'not-allowed', order: 'desc' }, // invalid orderBy
{ columnName: 'timestamp', order: 'desc' }, // invalid orderBy
];
const query = buildQuery({
groupBy: [
{
key: 'service.name',
dataType: DataTypes.String,
isColumn: true,
type: 'resource',
isJSON: false,
},
] as any,
orderBy,
});
const result = sanitizeOrderByForExplorer(query);
expect(result).toEqual([
{ columnName: 'service.name', order: 'asc' },
{ columnName: 'count()', order: 'desc' },
{ columnName: 'avg(duration)', order: 'asc' },
{ columnName: 'latency', order: 'asc' },
]);
});
it('returns empty when none of the orderBy items are allowed', () => {
(getParsedAggregationOptionsForOrderBy as jest.Mock).mockReturnValue([
{
key: 'count()',
dataType: DataTypes.Float64,
isColumn: false,
type: '',
isJSON: false,
},
]);
const query = buildQuery({
groupBy: [],
orderBy: [
{ columnName: 'foo', order: 'asc' },
{ columnName: 'bar', order: 'desc' },
],
});
const result = sanitizeOrderByForExplorer(query);
expect(result).toEqual([]);
});
it('handles missing orderBy by returning an empty array', () => {
(getParsedAggregationOptionsForOrderBy as jest.Mock).mockReturnValue([]);
const query = buildQuery({ orderBy: [] });
const result = sanitizeOrderByForExplorer(query);
expect(result).toEqual([]);
});
});

View File

@@ -0,0 +1,32 @@
import * as Sentry from '@sentry/react';
import {
IBuilderQuery,
OrderByPayload,
} from 'types/api/queryBuilder/queryBuilderData';
import { getParsedAggregationOptionsForOrderBy } from './aggregationConverter';
export function sanitizeOrderByForExplorer(
query: IBuilderQuery,
): OrderByPayload[] {
const allowed = new Set<string>();
(query.groupBy || []).forEach((g) => g?.key && allowed.add(g.key));
getParsedAggregationOptionsForOrderBy(query).forEach((agg) => {
// agg.key is the expression or alias (e.g., count(), avg(quantity), 'alias')
if ((agg as any)?.key) allowed.add((agg as any).key as string);
});
const current = query.orderBy || [];
const hasInvalidOrderBy = current.some((o) => !allowed.has(o.columnName));
if (hasInvalidOrderBy) {
Sentry.captureEvent({
message: `Invalid orderBy: current: ${JSON.stringify(
current,
)} - allowed: ${JSON.stringify(Array.from(allowed))}`,
level: 'warning',
});
}
return current.filter((o) => allowed.has(o.columnName));
}

View File

@@ -100,16 +100,36 @@ export function isFunctionOperator(operator: string): boolean {
const functionOperators = Object.values(QUERY_BUILDER_FUNCTIONS);
const sanitizedOperator = operator.trim();
// Check if it's a direct function operator
if (functionOperators.includes(sanitizedOperator)) {
// Check if it's a direct function operator (case-insensitive)
if (
functionOperators.some(
(func) => func.toLowerCase() === sanitizedOperator.toLowerCase(),
)
) {
return true;
}
// Check if it's a NOT function operator (e.g., "NOT has")
if (sanitizedOperator.toUpperCase().startsWith(OPERATORS.NOT)) {
const operatorWithoutNot = sanitizedOperator.substring(4).toLowerCase();
return functionOperators.includes(operatorWithoutNot);
return functionOperators.some(
(func) => func.toLowerCase() === operatorWithoutNot,
);
}
return false;
}
export function isNonValueOperator(operator: string): boolean {
const upperOperator = operator.toUpperCase();
// Check if it's a direct non-value operator
if (NON_VALUE_OPERATORS.includes(upperOperator)) {
return true;
}
// Check if it's a NOT non-value operator (e.g., "NOT EXISTS")
if (upperOperator.startsWith(OPERATORS.NOT)) {
const operatorWithoutNot = upperOperator.substring(4).trim(); // Remove "NOT " prefix
return NON_VALUE_OPERATORS.includes(operatorWithoutNot);
}
return false;
}

View File

@@ -9,6 +9,7 @@ import (
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
@@ -97,7 +98,12 @@ func (a *APIKey) Wrap(next http.Handler) http.Handler {
return
}
r = r.WithContext(ctx)
comment := ctxtypes.CommentFromContext(ctx)
comment.Set("auth_type", "api_key")
comment.Set("user_id", claims.UserID)
comment.Set("org_id", claims.OrgID)
r = r.WithContext(ctxtypes.NewContextWithComment(ctx, comment))
next.ServeHTTP(w, r)

View File

@@ -7,6 +7,7 @@ import (
"github.com/SigNoz/signoz/pkg/sharder"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
@@ -50,7 +51,12 @@ func (a *Auth) Wrap(next http.Handler) http.Handler {
return
}
r = r.WithContext(ctx)
comment := ctxtypes.CommentFromContext(ctx)
comment.Set("auth_type", "jwt")
comment.Set("user_id", claims.UserID)
comment.Set("org_id", claims.OrgID)
r = r.WithContext(ctxtypes.NewContextWithComment(ctx, comment))
next.ServeHTTP(w, r)
})

View File

@@ -0,0 +1,24 @@
package middleware
import (
"net/http"
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
)
type Comment struct{}
func NewComment() *Comment {
return &Comment{}
}
func (middleware *Comment) Wrap(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
comment := ctxtypes.CommentFromContext(req.Context())
comment.Merge(ctxtypes.CommentFromHTTPRequest(req))
req = req.WithContext(ctxtypes.NewContextWithComment(req.Context(), comment))
next.ServeHTTP(rw, req)
})
}

View File

@@ -2,16 +2,11 @@ package middleware
import (
"bytes"
"context"
"log/slog"
"net"
"net/http"
"net/url"
"strings"
"time"
"github.com/SigNoz/signoz/pkg/query-service/common"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/gorilla/mux"
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
)
@@ -55,9 +50,6 @@ func (middleware *Logging) Wrap(next http.Handler) http.Handler {
string(semconv.HTTPRouteKey), path,
}
logCommentKVs := middleware.getLogCommentKVs(req)
req = req.WithContext(context.WithValue(req.Context(), common.LogCommentKey, logCommentKVs))
badResponseBuffer := new(bytes.Buffer)
writer := newBadResponseLoggingWriter(rw, badResponseBuffer)
next.ServeHTTP(writer, req)
@@ -85,67 +77,3 @@ func (middleware *Logging) Wrap(next http.Handler) http.Handler {
}
})
}
func (middleware *Logging) getLogCommentKVs(r *http.Request) map[string]string {
referrer := r.Header.Get("Referer")
var path, dashboardID, alertID, page, client, viewName, tab string
if referrer != "" {
referrerURL, _ := url.Parse(referrer)
client = "browser"
path = referrerURL.Path
if strings.Contains(path, "/dashboard") {
// Split the path into segments
pathSegments := strings.Split(referrerURL.Path, "/")
// The dashboard ID should be the segment after "/dashboard/"
// Loop through pathSegments to find "dashboard" and then take the next segment as the ID
for i, segment := range pathSegments {
if segment == "dashboard" && i < len(pathSegments)-1 {
// Return the next segment, which should be the dashboard ID
dashboardID = pathSegments[i+1]
}
}
page = "dashboards"
} else if strings.Contains(path, "/alerts") {
urlParams := referrerURL.Query()
alertID = urlParams.Get("ruleId")
page = "alerts"
} else if strings.Contains(path, "logs") && strings.Contains(path, "explorer") {
page = "logs-explorer"
viewName = referrerURL.Query().Get("viewName")
} else if strings.Contains(path, "/trace") || strings.Contains(path, "traces-explorer") {
page = "traces-explorer"
viewName = referrerURL.Query().Get("viewName")
} else if strings.Contains(path, "/services") {
page = "services"
tab = referrerURL.Query().Get("tab")
if tab == "" {
tab = "OVER_METRICS"
}
} else if strings.Contains(path, "/metrics") {
page = "metrics-explorer"
}
} else {
client = "api"
}
var email string
claims, err := authtypes.ClaimsFromContext(r.Context())
if err == nil {
email = claims.Email
}
kvs := map[string]string{
"path": path,
"dashboardID": dashboardID,
"alertID": alertID,
"source": page,
"client": client,
"viewName": viewName,
"servicesTab": tab,
"email": email,
}
return kvs
}

View File

@@ -4,7 +4,6 @@ import (
"context"
"encoding/json"
"net/http"
"regexp"
"runtime/debug"
"github.com/SigNoz/signoz/pkg/analytics"
@@ -12,6 +11,7 @@ import (
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/SigNoz/signoz/pkg/variables"
@@ -166,49 +166,9 @@ func (a *API) logEvent(ctx context.Context, referrer string, event *qbtypes.QBEv
return
}
properties["referrer"] = referrer
logsExplorerMatched, _ := regexp.MatchString(`/logs/logs-explorer(?:\?.*)?$`, referrer)
traceExplorerMatched, _ := regexp.MatchString(`/traces-explorer(?:\?.*)?$`, referrer)
metricsExplorerMatched, _ := regexp.MatchString(`/metrics-explorer/explorer(?:\?.*)?$`, referrer)
dashboardMatched, _ := regexp.MatchString(`/dashboard/[a-zA-Z0-9\-]+/(new|edit)(?:\?.*)?$`, referrer)
alertMatched, _ := regexp.MatchString(`/alerts/(new|edit)(?:\?.*)?$`, referrer)
switch {
case dashboardMatched:
properties["module_name"] = "dashboard"
case alertMatched:
properties["module_name"] = "rule"
case metricsExplorerMatched:
properties["module_name"] = "metrics-explorer"
case logsExplorerMatched:
properties["module_name"] = "logs-explorer"
case traceExplorerMatched:
properties["module_name"] = "traces-explorer"
default:
return
}
if dashboardMatched {
if dashboardIDRegex, err := regexp.Compile(`/dashboard/([a-f0-9\-]+)/`); err == nil {
if matches := dashboardIDRegex.FindStringSubmatch(referrer); len(matches) > 1 {
properties["dashboard_id"] = matches[1]
}
}
if widgetIDRegex, err := regexp.Compile(`widgetId=([a-f0-9\-]+)`); err == nil {
if matches := widgetIDRegex.FindStringSubmatch(referrer); len(matches) > 1 {
properties["widget_id"] = matches[1]
}
}
}
if alertMatched {
if alertIDRegex, err := regexp.Compile(`ruleId=(\d+)`); err == nil {
if matches := alertIDRegex.FindStringSubmatch(referrer); len(matches) > 1 {
properties["rule_id"] = matches[1]
}
}
comments := ctxtypes.CommentFromContext(ctx).Map()
for key, value := range comments {
properties[key] = value
}
if !event.HasData {

View File

@@ -490,7 +490,6 @@ func (bc *bucketCache) mergeTimeSeriesValues(ctx context.Context, buckets []*cac
key string
}
seriesMap := make(map[seriesKey]*qbtypes.TimeSeries, estimatedSeries)
var queryName string
for _, bucket := range buckets {
var tsData *qbtypes.TimeSeriesData
@@ -499,11 +498,6 @@ func (bc *bucketCache) mergeTimeSeriesValues(ctx context.Context, buckets []*cac
continue
}
// Preserve the query name from the first bucket
if queryName == "" && tsData.QueryName != "" {
queryName = tsData.QueryName
}
for _, aggBucket := range tsData.Aggregations {
for _, series := range aggBucket.Series {
// Create series key from labels
@@ -549,7 +543,6 @@ func (bc *bucketCache) mergeTimeSeriesValues(ctx context.Context, buckets []*cac
// Convert map back to slice
result := &qbtypes.TimeSeriesData{
QueryName: queryName,
Aggregations: make([]*qbtypes.AggregationBucket, 0, len(aggMap)),
}
@@ -738,9 +731,7 @@ func (bc *bucketCache) trimResultToFluxBoundary(result *qbtypes.Result, fluxBoun
case qbtypes.RequestTypeTimeSeries:
// Trim time series data
if tsData, ok := result.Value.(*qbtypes.TimeSeriesData); ok && tsData != nil {
trimmedData := &qbtypes.TimeSeriesData{
QueryName: tsData.QueryName,
}
trimmedData := &qbtypes.TimeSeriesData{}
for _, aggBucket := range tsData.Aggregations {
trimmedBucket := &qbtypes.AggregationBucket{
@@ -807,7 +798,6 @@ func (bc *bucketCache) filterResultToTimeRange(result *qbtypes.Result, startMs,
case qbtypes.RequestTypeTimeSeries:
if tsData, ok := result.Value.(*qbtypes.TimeSeriesData); ok {
filteredData := &qbtypes.TimeSeriesData{
QueryName: tsData.QueryName,
Aggregations: make([]*qbtypes.AggregationBucket, 0, len(tsData.Aggregations)),
}

View File

@@ -169,9 +169,8 @@ func TestBucketCache_Put_And_Get(t *testing.T) {
assert.Equal(t, []string{"test warning"}, cached.Warnings)
// Verify the time series data
tsData, ok := cached.Value.(*qbtypes.TimeSeriesData)
_, ok := cached.Value.(*qbtypes.TimeSeriesData)
require.True(t, ok)
assert.Equal(t, "A", tsData.QueryName)
}
func TestBucketCache_PartialHit(t *testing.T) {
@@ -1077,7 +1076,6 @@ func TestBucketCache_FilteredCachedResults(t *testing.T) {
// Verify the cached result only contains values within the requested range
tsData, ok := cached.Value.(*qbtypes.TimeSeriesData)
require.True(t, ok)
assert.Equal(t, "A", tsData.QueryName)
require.Len(t, tsData.Aggregations, 1)
require.Len(t, tsData.Aggregations[0].Series, 1)

View File

@@ -42,8 +42,10 @@ 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, qbtypes.RequestTypeTrace:
case qbtypes.RequestTypeRaw:
payload, err = readAsRaw(rows, queryName)
case qbtypes.RequestTypeTrace:
payload, err = readAsTrace(rows, queryName)
// TODO: add support for other request types
}
@@ -332,6 +334,74 @@ 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,6 +29,8 @@ 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:
@@ -70,6 +72,10 @@ 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,15 +28,16 @@ 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]
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]
traceOperatorStmtBuilder qbtypes.TraceOperatorStatementBuilder
bucketCache BucketCache
}
var _ Querier = (*querier)(nil)
@@ -50,19 +51,21 @@ 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,
bucketCache: bucketCache,
logger: querierSettings.Logger(),
telemetryStore: telemetryStore,
metadataStore: metadataStore,
promEngine: promEngine,
traceStmtBuilder: traceStmtBuilder,
logStmtBuilder: logStmtBuilder,
metricStmtBuilder: metricStmtBuilder,
meterStmtBuilder: meterStmtBuilder,
traceOperatorStmtBuilder: traceOperatorStmtBuilder,
bucketCache: bucketCache,
}
}
@@ -124,9 +127,28 @@ func (q *querier) QueryRange(ctx context.Context, orgID valuer.UUID, req *qbtype
NumberOfQueries: len(req.CompositeQuery.Queries),
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 {
@@ -220,6 +242,21 @@ 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
}
}
}
@@ -240,6 +277,38 @@ 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)
@@ -256,6 +325,22 @@ func (q *querier) QueryRange(ctx context.Context, orgID valuer.UUID, req *qbtype
}
chSQLQuery := newchSQLQuery(q.logger, q.telemetryStore, chQuery, nil, qbtypes.TimeRange{From: req.Start, To: req.End}, req.RequestType, tmplVars)
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]:
@@ -385,6 +470,15 @@ func (q *querier) run(
if err != nil {
return nil, err
}
switch v := result.Value.(type) {
case *qbtypes.TimeSeriesData:
v.QueryName = name
case *qbtypes.ScalarData:
v.QueryName = name
case *qbtypes.RawData:
v.QueryName = name
}
results[name] = result.Value
warnings = append(warnings, result.Warnings...)
warningsDocURL = result.WarningsDocURL
@@ -569,7 +663,16 @@ func (q *querier) createRangedQuery(originalQuery qbtypes.Query, timeRange qbtyp
return newBuilderQuery(q.telemetryStore, q.meterStmtBuilder, specCopy, adjustedTimeRange, qt.kind, qt.variables)
}
return newBuilderQuery(q.telemetryStore, q.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,6 +89,16 @@ 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)
@@ -147,7 +157,7 @@ func newProvider(
cfg.FluxInterval,
)
// Create and return the querier
// Create and return the querier - ADD traceOperatorStmtBuilder parameter
return querier.New(
settings,
telemetryStore,
@@ -157,6 +167,7 @@ func newProvider(
logStmtBuilder,
metricStmtBuilder,
meterStmtBuilder,
traceOperatorStmtBuilder,
bucketCache,
), nil
}

View File

@@ -0,0 +1,145 @@
package querier
import (
"context"
"fmt"
"strings"
"time"
"github.com/ClickHouse/clickhouse-go/v2"
"github.com/SigNoz/signoz/pkg/telemetrystore"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
)
type traceOperatorQuery struct {
telemetryStore telemetrystore.TelemetryStore
stmtBuilder qbtypes.TraceOperatorStatementBuilder
spec qbtypes.QueryBuilderTraceOperator
compositeQuery *qbtypes.CompositeQuery
fromMS uint64
toMS uint64
kind qbtypes.RequestType
}
var _ qbtypes.Query = (*traceOperatorQuery)(nil)
func (q *traceOperatorQuery) Fingerprint() string {
if q.kind == qbtypes.RequestTypeRaw {
return ""
}
parts := []string{"trace_operator"}
parts = append(parts, fmt.Sprintf("expr=%s", q.spec.Expression))
// Add returnSpansFrom if specified
if q.spec.ReturnSpansFrom != "" {
parts = append(parts, fmt.Sprintf("return=%s", q.spec.ReturnSpansFrom))
}
// Add step interval if present
parts = append(parts, fmt.Sprintf("step=%s", q.spec.StepInterval.String()))
// Add filter if present
if q.spec.Filter != nil && q.spec.Filter.Expression != "" {
parts = append(parts, fmt.Sprintf("filter=%s", q.spec.Filter.Expression))
}
// Add aggregations
if len(q.spec.Aggregations) > 0 {
aggParts := []string{}
for _, agg := range q.spec.Aggregations {
aggParts = append(aggParts, agg.Expression)
}
parts = append(parts, fmt.Sprintf("aggs=[%s]", strings.Join(aggParts, ",")))
}
// Add group by
if len(q.spec.GroupBy) > 0 {
groupByParts := []string{}
for _, gb := range q.spec.GroupBy {
groupByParts = append(groupByParts, fingerprintGroupByKey(gb))
}
parts = append(parts, fmt.Sprintf("groupby=[%s]", strings.Join(groupByParts, ",")))
}
// Add order by
if len(q.spec.Order) > 0 {
orderParts := []string{}
for _, o := range q.spec.Order {
orderParts = append(orderParts, fingerprintOrderBy(o))
}
parts = append(parts, fmt.Sprintf("order=[%s]", strings.Join(orderParts, ",")))
}
// Add limit
if q.spec.Limit > 0 {
parts = append(parts, fmt.Sprintf("limit=%d", q.spec.Limit))
}
return strings.Join(parts, "&")
}
func (q *traceOperatorQuery) Window() (uint64, uint64) {
return q.fromMS, q.toMS
}
func (q *traceOperatorQuery) Execute(ctx context.Context) (*qbtypes.Result, error) {
stmt, err := q.stmtBuilder.Build(
ctx,
q.fromMS,
q.toMS,
q.kind,
q.spec,
q.compositeQuery,
)
if err != nil {
return nil, err
}
// Execute the query with proper context
result, err := q.executeWithContext(ctx, stmt.Query, stmt.Args)
if err != nil {
return nil, err
}
result.Warnings = stmt.Warnings
return result, nil
}
func (q *traceOperatorQuery) executeWithContext(ctx context.Context, query string, args []any) (*qbtypes.Result, error) {
totalRows := uint64(0)
totalBytes := uint64(0)
elapsed := time.Duration(0)
ctx = clickhouse.Context(ctx, clickhouse.WithProgress(func(p *clickhouse.Progress) {
totalRows += p.Rows
totalBytes += p.Bytes
elapsed += p.Elapsed
}))
rows, err := q.telemetryStore.ClickhouseDB().Query(ctx, query, args...)
if err != nil {
return nil, err
}
defer rows.Close()
// Pass query window and step for partial value detection
queryWindow := &qbtypes.TimeRange{From: q.fromMS, To: q.toMS}
// Use the consume function like builderQuery does
payload, err := consume(rows, q.kind, queryWindow, q.spec.StepInterval, q.spec.Name)
if err != nil {
return nil, err
}
return &qbtypes.Result{
Type: q.kind,
Value: payload,
Stats: qbtypes.ExecStats{
RowsScanned: totalRows,
BytesScanned: totalBytes,
DurationMS: uint64(elapsed.Milliseconds()),
},
}, nil
}

View File

@@ -3640,28 +3640,8 @@ func readRowsForTimeSeriesResult(rows driver.Rows, vars []interface{}, columnNam
return seriesList, getPersonalisedError(rows.Err())
}
func logCommentKVs(ctx context.Context) map[string]string {
kv := ctx.Value(common.LogCommentKey)
if kv == nil {
return nil
}
logCommentKVs, ok := kv.(map[string]string)
if !ok {
return nil
}
return logCommentKVs
}
// GetTimeSeriesResultV3 runs the query and returns list of time series
func (r *ClickHouseReader) GetTimeSeriesResultV3(ctx context.Context, query string) ([]*v3.Series, error) {
ctxArgs := map[string]interface{}{"query": query}
for k, v := range logCommentKVs(ctx) {
ctxArgs[k] = v
}
defer utils.Elapsed("GetTimeSeriesResultV3", ctxArgs)()
// Hook up query progress reporting if requested.
queryId := ctx.Value("queryId")
if queryId != nil {
@@ -3725,20 +3705,12 @@ func (r *ClickHouseReader) GetTimeSeriesResultV3(ctx context.Context, query stri
// GetListResultV3 runs the query and returns list of rows
func (r *ClickHouseReader) GetListResultV3(ctx context.Context, query string) ([]*v3.Row, error) {
ctxArgs := map[string]interface{}{"query": query}
for k, v := range logCommentKVs(ctx) {
ctxArgs[k] = v
}
defer utils.Elapsed("GetListResultV3", ctxArgs)()
rows, err := r.db.Query(ctx, query)
if err != nil {
zap.L().Error("error while reading time series result", zap.Error(err))
return nil, errors.New(err.Error())
}
defer rows.Close()
var (

View File

@@ -220,6 +220,7 @@ func (s *Server) createPublicServer(api *APIHandler, web web.Web) (*http.Server,
).Wrap)
r.Use(middleware.NewAPIKey(s.signoz.SQLStore, []string{"SIGNOZ-API-KEY"}, s.signoz.Instrumentation.Logger(), s.signoz.Sharder).Wrap)
r.Use(middleware.NewLogging(s.signoz.Instrumentation.Logger(), s.config.APIServer.Logging.ExcludedRoutes).Wrap)
r.Use(middleware.NewComment().Wrap)
am := middleware.NewAuthZ(s.signoz.Instrumentation.Logger())

View File

@@ -1,5 +0,0 @@
package common
type LogCommentContextKeyType string
const LogCommentKey LogCommentContextKeyType = "logComment"

View File

@@ -43,17 +43,17 @@ var (
// FromUnit returns a converter for the given unit
func FromUnit(u Unit) Converter {
switch u {
case "ns", "us", "µs", "ms", "s", "m", "h", "d":
case "ns", "us", "µs", "ms", "s", "m", "h", "d", "min":
return DurationConverter
case "bytes", "decbytes", "bits", "decbits", "kbytes", "decKbytes", "deckbytes", "mbytes", "decMbytes", "decmbytes", "gbytes", "decGbytes", "decgbytes", "tbytes", "decTbytes", "dectbytes", "pbytes", "decPbytes", "decpbytes":
case "bytes", "decbytes", "bits", "decbits", "kbytes", "decKbytes", "deckbytes", "mbytes", "decMbytes", "decmbytes", "gbytes", "decGbytes", "decgbytes", "tbytes", "decTbytes", "dectbytes", "pbytes", "decPbytes", "decpbytes", "By", "kBy", "MBy", "GBy", "TBy", "PBy":
return DataConverter
case "binBps", "Bps", "binbps", "bps", "KiBs", "Kibits", "KBs", "Kbits", "MiBs", "Mibits", "MBs", "Mbits", "GiBs", "Gibits", "GBs", "Gbits", "TiBs", "Tibits", "TBs", "Tbits", "PiBs", "Pibits", "PBs", "Pbits":
case "binBps", "Bps", "binbps", "bps", "KiBs", "Kibits", "KBs", "Kbits", "MiBs", "Mibits", "MBs", "Mbits", "GiBs", "Gibits", "GBs", "Gbits", "TiBs", "Tibits", "TBs", "Tbits", "PiBs", "Pibits", "PBs", "Pbits", "By/s", "kBy/s", "MBy/s", "GBy/s", "TBy/s", "PBy/s", "bit/s", "kbit/s", "Mbit/s", "Gbit/s", "Tbit/s", "Pbit/s":
return DataRateConverter
case "percent", "percentunit":
case "percent", "percentunit", "%":
return PercentConverter
case "bool", "bool_yes_no", "bool_true_false", "bool_1_0":
return BoolConverter
case "cps", "ops", "reqps", "rps", "wps", "iops", "cpm", "opm", "rpm", "wpm":
case "cps", "ops", "reqps", "rps", "wps", "iops", "cpm", "opm", "rpm", "wpm", "{count}/s", "{ops}/s", "{req}/s", "{read}/s", "{write}/s", "{iops}/s", "{count}/min", "{ops}/min", "{read}/min", "{write}/min":
return ThroughputConverter
default:
return NoneConverter

View File

@@ -60,7 +60,7 @@ func (*dataConverter) Name() string {
func FromDataUnit(u Unit) float64 {
switch u {
case "bytes": // base 2
case "bytes", "By": // base 2
return Byte
case "decbytes": // base 10
return Byte
@@ -68,23 +68,23 @@ func FromDataUnit(u Unit) float64 {
return Bit
case "decbits": // base 10
return Bit
case "kbytes": // base 2
case "kbytes", "kBy": // base 2
return Kibibyte
case "decKbytes", "deckbytes": // base 10
return Kilobyte
case "mbytes": // base 2
case "mbytes", "MBy": // base 2
return Mebibyte
case "decMbytes", "decmbytes": // base 10
return Megabyte
case "gbytes": // base 2
case "gbytes", "GBy": // base 2
return Gibibyte
case "decGbytes", "decgbytes": // base 10
return Gigabyte
case "tbytes": // base 2
case "tbytes", "TBy": // base 2
return Tebibyte
case "decTbytes", "dectbytes": // base 10
return Terabyte
case "pbytes": // base 2
case "pbytes", "PBy": // base 2
return Pebibyte
case "decPbytes", "decpbytes": // base 10
return Petabyte

View File

@@ -59,51 +59,51 @@ func FromDataRateUnit(u Unit) float64 {
switch u {
case "binBps": // bytes/sec(IEC)
return BytePerSecond
case "Bps": // bytes/sec(SI)
case "Bps", "By/s": // bytes/sec(SI)
return BytePerSecond
case "binbps": // bits/sec(IEC)
return BitPerSecond
case "bps": // bits/sec(SI)
case "bps", "bit/s": // bits/sec(SI)
return BitPerSecond
case "KiBs": // kibibytes/sec
return KibibytePerSecond
case "Kibits": // kibibits/sec
return KibibitPerSecond
case "KBs": // kilobytes/sec
case "KBs", "kBy/s": // kilobytes/sec
return KilobytePerSecond
case "Kbits": // kilobits/sec
case "Kbits", "kbit/s": // kilobits/sec
return KilobitPerSecond
case "MiBs": // mebibytes/sec
return MebibytePerSecond
case "Mibits": // mebibits/sec
return MebibitPerSecond
case "MBs": // megabytes/sec
case "MBs", "MBy/s": // megabytes/sec
return MegabytePerSecond
case "Mbits": // megabits/sec
case "Mbits", "Mbit/s": // megabits/sec
return MegabitPerSecond
case "GiBs": // gibibytes/sec
return GibibytePerSecond
case "Gibits": // gibibits/sec
return GibibitPerSecond
case "GBs": // gigabytes/sec
case "GBs", "GBy/s": // gigabytes/sec
return GigabytePerSecond
case "Gbits": // gigabits/sec
case "Gbits", "Gbit/s": // gigabits/sec
return GigabitPerSecond
case "TiBs": // tebibytes/sec
return TebibytePerSecond
case "Tibits": // tebibits/sec
return TebibitPerSecond
case "TBs": // terabytes/sec
case "TBs", "TBy/s": // terabytes/sec
return TerabytePerSecond
case "Tbits": // terabits/sec
case "Tbits", "Tbit/s": // terabits/sec
return TerabitPerSecond
case "PiBs": // pebibytes/sec
return PebibytePerSecond
case "Pibits": // pebibits/sec
return PebibitPerSecond
case "PBs": // petabytes/sec
case "PBs", "PBy/s": // petabytes/sec
return PetabytePerSecond
case "Pbits": // petabits/sec
case "Pbits", "Pbit/s": // petabits/sec
return PetabitPerSecond
default:
return 1

View File

@@ -36,10 +36,16 @@ func TestDataRate(t *testing.T) {
// 8 bits = 1 byte
assert.Equal(t, Value{F: 1, U: "binBps"}, dataRateConverter.Convert(Value{F: 8, U: "binbps"}, "binBps"))
// 8 bits = 1 byte
assert.Equal(t, Value{F: 1, U: "Bps"}, dataRateConverter.Convert(Value{F: 8, U: "bps"}, "Bps"))
// 8 bits = 1 byte
assert.Equal(t, Value{F: 1, U: "By/s"}, dataRateConverter.Convert(Value{F: 8, U: "bit/s"}, "By/s"))
// 1024 bytes = 1 kbytes
assert.Equal(t, Value{F: 1, U: "KiBs"}, dataRateConverter.Convert(Value{F: 1024, U: "binBps"}, "KiBs"))
// 1 byte = 8 bits
assert.Equal(t, Value{F: 8, U: "binbps"}, dataRateConverter.Convert(Value{F: 1, U: "binBps"}, "binbps"))
// 1 byte = 8 bits
assert.Equal(t, Value{F: 8, U: "bit/s"}, dataRateConverter.Convert(Value{F: 1, U: "Bps"}, "bit/s"))
// 1 mbytes = 1024 kbytes
assert.Equal(t, Value{F: 1, U: "MiBs"}, dataRateConverter.Convert(Value{F: 1024, U: "KiBs"}, "MiBs"))
// 1 kbytes = 1024 bytes
@@ -57,6 +63,10 @@ func TestDataRate(t *testing.T) {
// 1 gbytes = 1024 * 1024 kbytes
assert.Equal(t, Value{F: 1024 * 1024, U: "KiBs"}, dataRateConverter.Convert(Value{F: 1, U: "GiBs"}, "KiBs"))
// 1 gbytes = 1024 * 1024 * 1024 bytes
assert.Equal(t, Value{F: (1024 * 1024 * 1024 * 8) / 1024, U: "Kibits"}, dataRateConverter.Convert(Value{F: 1, U: "GiBs"}, "Kibits"))
// 1 gbytes = 1024 * 1024 * 1024 bytes
assert.Equal(t, Value{F: float64(1024*1024*1024) / 1000.0, U: "kBy/s"}, dataRateConverter.Convert(Value{F: 1, U: "GiBs"}, "kBy/s"))
// 1 gbytes = 1024 * 1024 * 1024 bytes
assert.Equal(t, Value{F: 1024 * 1024 * 1024, U: "binBps"}, dataRateConverter.Convert(Value{F: 1, U: "GiBs"}, "binBps"))
// 1024 * 1024 bytes = 1 mbytes
assert.Equal(t, Value{F: 1, U: "MiBs"}, dataRateConverter.Convert(Value{F: 1024 * 1024, U: "binBps"}, "MiBs"))

View File

@@ -10,8 +10,10 @@ func TestData(t *testing.T) {
dataConverter := NewDataConverter()
// 8 bits = 1 byte
assert.Equal(t, Value{F: 1, U: "bytes"}, dataConverter.Convert(Value{F: 8, U: "bits"}, "bytes"))
assert.Equal(t, Value{F: 1, U: "By"}, dataConverter.Convert(Value{F: 8, U: "bits"}, "By"))
// 1024 bytes = 1 kbytes
assert.Equal(t, Value{F: 1, U: "kbytes"}, dataConverter.Convert(Value{F: 1024, U: "bytes"}, "kbytes"))
assert.Equal(t, Value{F: 1, U: "kBy"}, dataConverter.Convert(Value{F: 1024, U: "bytes"}, "kBy"))
// 1 byte = 8 bits
assert.Equal(t, Value{F: 8, U: "bits"}, dataConverter.Convert(Value{F: 1, U: "bytes"}, "bits"))
// 1 mbytes = 1024 kbytes
@@ -20,6 +22,7 @@ func TestData(t *testing.T) {
assert.Equal(t, Value{F: 1024, U: "bytes"}, dataConverter.Convert(Value{F: 1, U: "kbytes"}, "bytes"))
// 1024 kbytes = 1 mbytes
assert.Equal(t, Value{F: 1, U: "mbytes"}, dataConverter.Convert(Value{F: 1024, U: "kbytes"}, "mbytes"))
assert.Equal(t, Value{F: 1, U: "MBy"}, dataConverter.Convert(Value{F: 1024, U: "kbytes"}, "MBy"))
// 1 mbytes = 1024 * 1024 bytes
assert.Equal(t, Value{F: 1024 * 1024, U: "bytes"}, dataConverter.Convert(Value{F: 1, U: "mbytes"}, "bytes"))
// 1024 mbytes = 1 gbytes
@@ -42,6 +45,10 @@ func TestData(t *testing.T) {
assert.Equal(t, Value{F: 1024 * 1024 * 1024 * 1024, U: "bytes"}, dataConverter.Convert(Value{F: 1, U: "tbytes"}, "bytes"))
// 1024 tbytes = 1 pbytes
assert.Equal(t, Value{F: 1, U: "pbytes"}, dataConverter.Convert(Value{F: 1024, U: "tbytes"}, "pbytes"))
// 1024 tbytes = 1 pbytes
assert.Equal(t, Value{F: 1, U: "PBy"}, dataConverter.Convert(Value{F: 1024, U: "tbytes"}, "PBy"))
// 1 pbytes = 1024 tbytes
assert.Equal(t, Value{F: 1024, U: "tbytes"}, dataConverter.Convert(Value{F: 1, U: "pbytes"}, "tbytes"))
// 1024 pbytes = 1 tbytes
assert.Equal(t, Value{F: 1024, U: "TBy"}, dataConverter.Convert(Value{F: 1, U: "pbytes"}, "TBy"))
}

View File

@@ -13,7 +13,7 @@ func (*percentConverter) Name() string {
func FromPercentUnit(u Unit) float64 {
switch u {
case "percent":
case "percent", "%":
return 1
case "percentunit":
return 100

View File

@@ -13,4 +13,5 @@ func TestPercentConverter(t *testing.T) {
assert.Equal(t, Value{F: 100, U: "percent"}, percentConverter.Convert(Value{F: 1, U: "percentunit"}, "percent"))
assert.Equal(t, Value{F: 1, U: "percentunit"}, percentConverter.Convert(Value{F: 100, U: "percent"}, "percentunit"))
assert.Equal(t, Value{F: 0.01, U: "percentunit"}, percentConverter.Convert(Value{F: 1, U: "percent"}, "percentunit"))
assert.Equal(t, Value{F: 1, U: "percent"}, percentConverter.Convert(Value{F: 1, U: "%"}, "percent"))
}

View File

@@ -41,7 +41,7 @@ func FromTimeUnit(u Unit) Duration {
return Decisecond
case "s":
return Second
case "m":
case "m", "min":
return Minute
case "h":
return Hour

View File

@@ -24,6 +24,8 @@ func TestDurationConvert(t *testing.T) {
assert.Equal(t, Value{F: 60, U: "s"}, timeConverter.Convert(Value{F: 1, U: "m"}, "s"))
// 60 m = 1 h
assert.Equal(t, Value{F: 1, U: "h"}, timeConverter.Convert(Value{F: 60, U: "m"}, "h"))
// 60 min = 1 h
assert.Equal(t, Value{F: 1, U: "h"}, timeConverter.Convert(Value{F: 60, U: "min"}, "h"))
// 168 h = 1 w
assert.Equal(t, Value{F: 1, U: "w"}, timeConverter.Convert(Value{F: 168, U: "h"}, "w"))
// 1 h = 60 m

View File

@@ -20,7 +20,7 @@ func (*dataFormatter) Name() string {
func (f *dataFormatter) Format(value float64, unit string) string {
switch unit {
case "bytes":
case "bytes", "By":
return humanize.IBytes(uint64(value))
case "decbytes":
return humanize.Bytes(uint64(value))
@@ -28,23 +28,23 @@ func (f *dataFormatter) Format(value float64, unit string) string {
return humanize.IBytes(uint64(value * converter.Bit))
case "decbits":
return humanize.Bytes(uint64(value * converter.Bit))
case "kbytes":
case "kbytes", "kBy":
return humanize.IBytes(uint64(value * converter.Kibibit))
case "decKbytes", "deckbytes":
return humanize.IBytes(uint64(value * converter.Kilobit))
case "mbytes":
case "mbytes", "MBy":
return humanize.IBytes(uint64(value * converter.Mebibit))
case "decMbytes", "decmbytes":
return humanize.Bytes(uint64(value * converter.Megabit))
case "gbytes":
case "gbytes", "GBy":
return humanize.IBytes(uint64(value * converter.Gibibit))
case "decGbytes", "decgbytes":
return humanize.Bytes(uint64(value * converter.Gigabit))
case "tbytes":
case "tbytes", "TBy":
return humanize.IBytes(uint64(value * converter.Tebibit))
case "decTbytes", "dectbytes":
return humanize.Bytes(uint64(value * converter.Terabit))
case "pbytes":
case "pbytes", "PBy":
return humanize.IBytes(uint64(value * converter.Pebibit))
case "decPbytes", "decpbytes":
return humanize.Bytes(uint64(value * converter.Petabit))

View File

@@ -22,51 +22,51 @@ func (f *dataRateFormatter) Format(value float64, unit string) string {
switch unit {
case "binBps":
return humanize.IBytes(uint64(value)) + "/s"
case "Bps":
case "Bps", "By/s":
return humanize.Bytes(uint64(value)) + "/s"
case "binbps":
return humanize.IBytes(uint64(value*converter.BitPerSecond)) + "/s"
case "bps":
case "bps", "bit/s":
return humanize.Bytes(uint64(value*converter.BitPerSecond)) + "/s"
case "KiBs":
return humanize.IBytes(uint64(value*converter.KibibitPerSecond)) + "/s"
case "Kibits":
return humanize.IBytes(uint64(value*converter.KibibytePerSecond)) + "/s"
case "KBs":
case "KBs", "kBy/s":
return humanize.IBytes(uint64(value*converter.KilobitPerSecond)) + "/s"
case "Kbits":
case "Kbits", "kbit/s":
return humanize.IBytes(uint64(value*converter.KilobytePerSecond)) + "/s"
case "MiBs":
return humanize.IBytes(uint64(value*converter.MebibitPerSecond)) + "/s"
case "Mibits":
return humanize.IBytes(uint64(value*converter.MebibytePerSecond)) + "/s"
case "MBs":
case "MBs", "MBy/s":
return humanize.IBytes(uint64(value*converter.MegabitPerSecond)) + "/s"
case "Mbits":
case "Mbits", "Mbit/s":
return humanize.IBytes(uint64(value*converter.MegabytePerSecond)) + "/s"
case "GiBs":
return humanize.IBytes(uint64(value*converter.GibibitPerSecond)) + "/s"
case "Gibits":
return humanize.IBytes(uint64(value*converter.GibibytePerSecond)) + "/s"
case "GBs":
case "GBs", "GBy/s":
return humanize.IBytes(uint64(value*converter.GigabitPerSecond)) + "/s"
case "Gbits":
case "Gbits", "Gbit/s":
return humanize.IBytes(uint64(value*converter.GigabytePerSecond)) + "/s"
case "TiBs":
return humanize.IBytes(uint64(value*converter.TebibitPerSecond)) + "/s"
case "Tibits":
return humanize.IBytes(uint64(value*converter.TebibytePerSecond)) + "/s"
case "TBs":
case "TBs", "TBy/s":
return humanize.IBytes(uint64(value*converter.TerabitPerSecond)) + "/s"
case "Tbits":
case "Tbits", "Tbit/s":
return humanize.IBytes(uint64(value*converter.TerabytePerSecond)) + "/s"
case "PiBs":
return humanize.IBytes(uint64(value*converter.PebibitPerSecond)) + "/s"
case "Pibits":
return humanize.IBytes(uint64(value*converter.PebibytePerSecond)) + "/s"
case "PBs":
case "PBs", "PBy/s":
return humanize.IBytes(uint64(value*converter.PetabitPerSecond)) + "/s"
case "Pbits":
case "Pbits", "Pbit/s":
return humanize.IBytes(uint64(value*converter.PetabytePerSecond)) + "/s"
}
// When unit is not matched, return the value as it is.

View File

@@ -10,14 +10,25 @@ func TestData(t *testing.T) {
dataFormatter := NewDataFormatter()
assert.Equal(t, "1 B", dataFormatter.Format(1, "bytes"))
assert.Equal(t, "1 B", dataFormatter.Format(1, "By"))
assert.Equal(t, "1.0 KiB", dataFormatter.Format(1024, "bytes"))
assert.Equal(t, "1.0 KiB", dataFormatter.Format(1024, "By"))
assert.Equal(t, "2.3 GiB", dataFormatter.Format(2.3*1024, "mbytes"))
assert.Equal(t, "2.3 GiB", dataFormatter.Format(2.3*1024, "MBy"))
assert.Equal(t, "1.0 MiB", dataFormatter.Format(1024*1024, "bytes"))
assert.Equal(t, "1.0 MiB", dataFormatter.Format(1024*1024, "By"))
assert.Equal(t, "69 TiB", dataFormatter.Format(69*1024*1024, "mbytes"))
assert.Equal(t, "69 TiB", dataFormatter.Format(69*1024*1024, "MBy"))
assert.Equal(t, "102 KiB", dataFormatter.Format(102*1024, "bytes"))
assert.Equal(t, "102 KiB", dataFormatter.Format(102*1024, "By"))
assert.Equal(t, "240 MiB", dataFormatter.Format(240*1024, "kbytes"))
assert.Equal(t, "240 MiB", dataFormatter.Format(240*1024, "kBy"))
assert.Equal(t, "1.0 GiB", dataFormatter.Format(1024*1024, "kbytes"))
assert.Equal(t, "1.0 GiB", dataFormatter.Format(1024*1024, "kBy"))
assert.Equal(t, "23 GiB", dataFormatter.Format(23*1024*1024, "kbytes"))
assert.Equal(t, "23 GiB", dataFormatter.Format(23*1024*1024, "kBy"))
assert.Equal(t, "32 TiB", dataFormatter.Format(32*1024*1024*1024, "kbytes"))
assert.Equal(t, "32 TiB", dataFormatter.Format(32*1024*1024*1024, "kBy"))
assert.Equal(t, "24 MiB", dataFormatter.Format(24, "mbytes"))
assert.Equal(t, "24 MiB", dataFormatter.Format(24, "MBy"))
}

View File

@@ -18,17 +18,17 @@ var (
func FromUnit(u string) Formatter {
switch u {
case "ns", "us", "µs", "ms", "s", "m", "h", "d":
case "ns", "us", "µs", "ms", "s", "m", "h", "d", "min":
return DurationFormatter
case "bytes", "decbytes", "bits", "decbits", "kbytes", "decKbytes", "deckbytes", "mbytes", "decMbytes", "decmbytes", "gbytes", "decGbytes", "decgbytes", "tbytes", "decTbytes", "dectbytes", "pbytes", "decPbytes", "decpbytes":
case "bytes", "decbytes", "bits", "decbits", "kbytes", "decKbytes", "deckbytes", "mbytes", "decMbytes", "decmbytes", "gbytes", "decGbytes", "decgbytes", "tbytes", "decTbytes", "dectbytes", "pbytes", "decPbytes", "decpbytes", "By", "kBy", "MBy", "GBy", "TBy", "PBy":
return DataFormatter
case "binBps", "Bps", "binbps", "bps", "KiBs", "Kibits", "KBs", "Kbits", "MiBs", "Mibits", "MBs", "Mbits", "GiBs", "Gibits", "GBs", "Gbits", "TiBs", "Tibits", "TBs", "Tbits", "PiBs", "Pibits", "PBs", "Pbits":
case "binBps", "Bps", "binbps", "bps", "KiBs", "Kibits", "KBs", "Kbits", "MiBs", "Mibits", "MBs", "Mbits", "GiBs", "Gibits", "GBs", "Gbits", "TiBs", "Tibits", "TBs", "Tbits", "PiBs", "Pibits", "PBs", "Pbits", "By/s", "kBy/s", "MBy/s", "GBy/s", "TBy/s", "PBy/s", "bit/s", "kbit/s", "Mbit/s", "Gbit/s", "Tbit/s", "Pbit/s":
return DataRateFormatter
case "percent", "percentunit":
case "percent", "percentunit", "%":
return PercentFormatter
case "bool", "bool_yes_no", "bool_true_false", "bool_1_0":
return BoolFormatter
case "cps", "ops", "reqps", "rps", "wps", "iops", "cpm", "opm", "rpm", "wpm":
case "cps", "ops", "reqps", "rps", "wps", "iops", "cpm", "opm", "rpm", "wpm", "{count}/s", "{ops}/s", "{req}/s", "{read}/s", "{write}/s", "{iops}/s", "{count}/min", "{ops}/min", "{read}/min", "{write}/min":
return ThroughputFormatter
default:
return NoneFormatter

View File

@@ -22,7 +22,7 @@ func toPercentUnit(value float64, decimals DecimalCount) string {
func (f *percentFormatter) Format(value float64, unit string) string {
switch unit {
case "percent":
case "percent", "%":
return toPercent(value, nil)
case "percentunit":
return toPercentUnit(value, nil)

View File

@@ -22,25 +22,25 @@ func simpleCountUnit(value float64, decimals *int, symbol string) string {
func (f *throughputFormatter) Format(value float64, unit string) string {
switch unit {
case "cps":
case "cps", "{count}/s":
return simpleCountUnit(value, nil, "c/s")
case "ops":
case "ops", "{ops}/s":
return simpleCountUnit(value, nil, "op/s")
case "reqps":
case "reqps", "{req}/s":
return simpleCountUnit(value, nil, "req/s")
case "rps":
case "rps", "{read}/s":
return simpleCountUnit(value, nil, "r/s")
case "wps":
case "wps", "{write}/s":
return simpleCountUnit(value, nil, "w/s")
case "iops":
case "iops", "{iops}/s":
return simpleCountUnit(value, nil, "iops")
case "cpm":
case "cpm", "{count}/min":
return simpleCountUnit(value, nil, "c/m")
case "opm":
case "opm", "{ops}/min":
return simpleCountUnit(value, nil, "op/m")
case "rpm":
case "rpm", "{read}/min":
return simpleCountUnit(value, nil, "r/m")
case "wpm":
case "wpm", "{write}/min":
return simpleCountUnit(value, nil, "w/m")
}
// When unit is not matched, return the value as it is.

View File

@@ -10,6 +10,11 @@ func TestThroughput(t *testing.T) {
throughputFormatter := NewThroughputFormatter()
assert.Equal(t, "10 req/s", throughputFormatter.Format(10, "reqps"))
assert.Equal(t, "10 req/s", throughputFormatter.Format(10, "{req}/s"))
assert.Equal(t, "1K req/s", throughputFormatter.Format(1000, "reqps"))
assert.Equal(t, "1K req/s", throughputFormatter.Format(1000, "{req}/s"))
assert.Equal(t, "1M req/s", throughputFormatter.Format(1000000, "reqps"))
assert.Equal(t, "1M req/s", throughputFormatter.Format(1000000, "{req}/s"))
assert.Equal(t, "10 c/m", throughputFormatter.Format(10, "cpm"))
assert.Equal(t, "10 c/m", throughputFormatter.Format(10, "{count}/min"))
}

View File

@@ -26,7 +26,7 @@ func (f *durationFormatter) Format(value float64, unit string) string {
return toMilliSeconds(value)
case "s":
return toSeconds(value)
case "m":
case "m", "min":
return toMinutes(value)
case "h":
return toHours(value)

View File

@@ -26,4 +26,5 @@ func TestDuration(t *testing.T) {
assert.Equal(t, "1.82 min", durationFormatter.Format(109200000000, "ns"))
assert.Equal(t, "1.27 day", durationFormatter.Format(109800000000000, "ns"))
assert.Equal(t, "2 day", durationFormatter.Format(172800000, "ms"))
assert.Equal(t, "1 hour", durationFormatter.Format(60, "min"))
}

View File

@@ -7,7 +7,7 @@ import (
"sync"
"time"
"github.com/SigNoz/signoz/pkg/query-service/common"
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
ruletypes "github.com/SigNoz/signoz/pkg/types/ruletypes"
"github.com/SigNoz/signoz/pkg/valuer"
opentracing "github.com/opentracing/opentracing-go"
@@ -369,12 +369,10 @@ func (g *PromRuleTask) Eval(ctx context.Context, ts time.Time) {
rule.SetEvaluationTimestamp(t)
}(time.Now())
kvs := map[string]string{
"alertID": rule.ID(),
"source": "alerts",
"client": "query-service",
}
ctx = context.WithValue(ctx, common.LogCommentKey, kvs)
comment := ctxtypes.CommentFromContext(ctx)
comment.Set("rule_id", rule.ID())
comment.Set("auth_type", "internal")
ctx = ctxtypes.NewContextWithComment(ctx, comment)
_, err := rule.Eval(ctx, ts)
if err != nil {

View File

@@ -7,8 +7,8 @@ import (
"sync"
"time"
"github.com/SigNoz/signoz/pkg/query-service/common"
"github.com/SigNoz/signoz/pkg/query-service/utils/labels"
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
ruletypes "github.com/SigNoz/signoz/pkg/types/ruletypes"
"github.com/SigNoz/signoz/pkg/valuer"
opentracing "github.com/opentracing/opentracing-go"
@@ -352,12 +352,10 @@ func (g *RuleTask) Eval(ctx context.Context, ts time.Time) {
rule.SetEvaluationTimestamp(t)
}(time.Now())
kvs := map[string]string{
"alertID": rule.ID(),
"source": "alerts",
"client": "query-service",
}
ctx = context.WithValue(ctx, common.LogCommentKey, kvs)
comment := ctxtypes.CommentFromContext(ctx)
comment.Set("rule_id", rule.ID())
comment.Set("auth_type", "internal")
ctx = ctxtypes.NewContextWithComment(ctx, comment)
_, err := rule.Eval(ctx, ts)
if err != nil {

View File

@@ -1334,6 +1334,35 @@ func TestThresholdRuleUnitCombinations(t *testing.T) {
matchType: "1", // Once
target: 200, // 200 GB
},
{
targetUnit: "decgbytes",
yAxisUnit: "By",
values: [][]interface{}{
{float64(2863284053), "attr", time.Now()}, // 2.86 GB
{float64(2863388842), "attr", time.Now().Add(1 * time.Second)}, // 2.86 GB
{float64(300947400), "attr", time.Now().Add(2 * time.Second)}, // 0.3 GB
{float64(299316000), "attr", time.Now().Add(3 * time.Second)}, // 0.3 GB
{float64(66640400.00000001), "attr", time.Now().Add(4 * time.Second)}, // 66.64 MB
},
expectAlerts: 0,
compareOp: "1", // Above
matchType: "1", // Once
target: 200, // 200 GB
},
{
targetUnit: "h",
yAxisUnit: "min",
values: [][]interface{}{
{float64(55), "attr", time.Now()}, // 55 minutes
{float64(57), "attr", time.Now().Add(1 * time.Minute)}, // 57 minutes
{float64(30), "attr", time.Now().Add(2 * time.Minute)}, // 30 minutes
{float64(29), "attr", time.Now().Add(3 * time.Minute)}, // 29 minutes
},
expectAlerts: 0,
compareOp: "1", // Above
matchType: "1", // Once
target: 1, // 1 hour
},
}
logger := instrumentationtest.New().Logger()

View File

@@ -86,5 +86,26 @@ var (
matchType: "1", // Once
target: 200, // 200 GB
},
{
targetUnit: "decgbytes",
yAxisUnit: "By",
values: [][]interface{}{
{float64(2863284053), "attr", time.Now()}, // 2.86 GB
{float64(2863388842), "attr", time.Now().Add(1 * time.Second)}, // 2.86 GB
{float64(300947400), "attr", time.Now().Add(2 * time.Second)}, // 0.3 GB
{float64(299316000), "attr", time.Now().Add(3 * time.Second)}, // 0.3 GB
{float64(66640400.00000001), "attr", time.Now().Add(4 * time.Second)}, // 66.64 MB
},
metaValues: [][]interface{}{},
createTableValues: [][]interface{}{
{"statement"},
},
attrMetaValues: [][]interface{}{},
resourceMetaValues: [][]interface{}{},
expectAlerts: 0,
compareOp: "1", // Above
matchType: "1", // Once
target: 200, // 200 GB
},
}
)

View File

@@ -2,13 +2,12 @@ package telemetrystorehook
import (
"context"
"encoding/json"
"strings"
"github.com/ClickHouse/clickhouse-go/v2"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/query-service/common"
"github.com/SigNoz/signoz/pkg/telemetrystore"
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
)
type provider struct {
@@ -32,11 +31,7 @@ func NewSettings(ctx context.Context, providerSettings factory.ProviderSettings,
func (h *provider) BeforeQuery(ctx context.Context, _ *telemetrystore.QueryEvent) context.Context {
settings := clickhouse.Settings{}
// Apply default settings
logComment := h.getLogComment(ctx)
if logComment != "" {
settings["log_comment"] = logComment
}
settings["log_comment"] = ctxtypes.CommentFromContext(ctx).String()
if ctx.Value("enforce_max_result_rows") != nil {
settings["max_result_rows"] = h.settings.MaxResultRows
@@ -91,22 +86,4 @@ func (h *provider) BeforeQuery(ctx context.Context, _ *telemetrystore.QueryEvent
return ctx
}
func (h *provider) AfterQuery(ctx context.Context, event *telemetrystore.QueryEvent) {
}
func (h *provider) getLogComment(ctx context.Context) string {
// Get the key-value pairs from context for log comment
kv := ctx.Value(common.LogCommentKey)
if kv == nil {
return ""
}
logCommentKVs, ok := kv.(map[string]string)
if !ok {
return ""
}
logComment, _ := json.Marshal(logCommentKVs)
return string(logComment)
}
func (h *provider) AfterQuery(ctx context.Context, event *telemetrystore.QueryEvent) {}

View File

@@ -0,0 +1,959 @@
package telemetrytraces
import (
"context"
"fmt"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/querybuilder"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/huandu/go-sqlbuilder"
"strings"
)
type cteNode struct {
name string
sql string
args []any
dependsOn []string
}
type traceOperatorCTEBuilder struct {
ctx context.Context
start uint64
end uint64
operator *qbtypes.QueryBuilderTraceOperator
stmtBuilder *traceOperatorStatementBuilder
queries map[string]*qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]
ctes []cteNode
cteNameToIndex map[string]int
queryToCTEName map[string]string
compositeQuery *qbtypes.CompositeQuery
}
func (b *traceOperatorCTEBuilder) collectQueries() error {
referencedQueries := b.operator.CollectReferencedQueries(b.operator.ParsedExpression)
for _, queryEnv := range b.compositeQuery.Queries {
if queryEnv.Type == qbtypes.QueryTypeBuilder {
if traceQuery, ok := queryEnv.Spec.(qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]); ok {
for _, refName := range referencedQueries {
if traceQuery.Name == refName {
queryCopy := traceQuery
b.queries[refName] = &queryCopy
break
}
}
}
}
}
for _, refName := range referencedQueries {
if _, found := b.queries[refName]; !found {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "referenced query '%s' not found", refName)
}
}
return nil
}
func (b *traceOperatorCTEBuilder) build(requestType qbtypes.RequestType) (*qbtypes.Statement, error) {
if len(b.queries) == 0 {
if err := b.collectQueries(); err != nil {
return nil, err
}
}
err := b.buildBaseSpansCTE()
if err != nil {
return nil, err
}
rootCTEName, err := b.buildExpressionCTEs(b.operator.ParsedExpression)
if err != nil {
return nil, err
}
selectFromCTE := rootCTEName
if b.operator.ReturnSpansFrom != "" {
selectFromCTE = b.queryToCTEName[b.operator.ReturnSpansFrom]
if selectFromCTE == "" {
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput,
"returnSpansFrom references query '%s' which has no corresponding CTE",
b.operator.ReturnSpansFrom)
}
}
finalStmt, err := b.buildFinalQuery(selectFromCTE, requestType)
if err != nil {
return nil, err
}
var cteFragments []string
var cteArgs [][]any
timeConstantsCTE := b.buildTimeConstantsCTE()
cteFragments = append(cteFragments, timeConstantsCTE)
for _, cte := range b.ctes {
cteFragments = append(cteFragments, fmt.Sprintf("%s AS (%s)", cte.name, cte.sql))
cteArgs = append(cteArgs, cte.args)
}
finalSQL := querybuilder.CombineCTEs(cteFragments) + finalStmt.Query
finalArgs := querybuilder.PrependArgs(cteArgs, finalStmt.Args)
return &qbtypes.Statement{
Query: finalSQL,
Args: finalArgs,
Warnings: finalStmt.Warnings,
}, nil
}
func (b *traceOperatorCTEBuilder) buildTimeConstantsCTE() string {
startBucket := b.start/querybuilder.NsToSeconds - querybuilder.BucketAdjustment
endBucket := b.end / querybuilder.NsToSeconds
return fmt.Sprintf(`
toDateTime64(%d, 9) AS t_from,
toDateTime64(%d, 9) AS t_to,
%d AS bucket_from,
%d AS bucket_to`,
b.start, b.end, startBucket, endBucket)
}
func (b *traceOperatorCTEBuilder) buildBaseSpansCTE() error {
sb := sqlbuilder.NewSelectBuilder()
sb.Select(
"trace_id",
"span_id",
"parent_span_id",
"name",
"timestamp",
"duration_nano",
sqlbuilder.Escape("resource_string_service$$name")+" AS `service.name`",
sqlbuilder.Escape("resource_string_service$$name"),
sqlbuilder.Escape("resource_string_service$$name_exists"),
"attributes_string",
"attributes_number",
"attributes_bool",
"resources_string",
)
sb.From(fmt.Sprintf("%s.%s", DBName, SpanIndexV3TableName))
startBucket := b.start/querybuilder.NsToSeconds - querybuilder.BucketAdjustment
endBucket := b.end / querybuilder.NsToSeconds
sb.Where(
sb.GE("timestamp", fmt.Sprintf("%d", b.start)),
sb.L("timestamp", fmt.Sprintf("%d", b.end)),
sb.GE("ts_bucket_start", startBucket),
sb.LE("ts_bucket_start", endBucket),
)
sql, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
b.addCTE("base_spans", sql, args, nil)
return nil
}
func (b *traceOperatorCTEBuilder) buildExpressionCTEs(expr *qbtypes.TraceOperand) (string, error) {
if expr == nil {
return "", errors.NewInvalidInputf(errors.CodeInvalidInput, "expression is nil")
}
if expr.QueryRef != nil {
return b.buildQueryCTE(expr.QueryRef.Name)
}
var leftCTE, rightCTE string
var err error
if expr.Left != nil {
leftCTE, err = b.buildExpressionCTEs(expr.Left)
if err != nil {
return "", err
}
}
if expr.Right != nil {
rightCTE, err = b.buildExpressionCTEs(expr.Right)
if err != nil {
return "", err
}
}
return b.buildOperatorCTE(*expr.Operator, leftCTE, rightCTE)
}
func (b *traceOperatorCTEBuilder) buildQueryCTE(queryName string) (string, error) {
query, exists := b.queries[queryName]
if !exists {
return "", errors.NewInvalidInputf(errors.CodeInvalidInput, "query %s not found", queryName)
}
cteName := queryName
b.queryToCTEName[queryName] = cteName
if _, exists := b.cteNameToIndex[cteName]; exists {
return cteName, nil
}
keySelectors := getKeySelectors(*query)
keys, _, err := b.stmtBuilder.metadataStore.GetKeysMulti(b.ctx, keySelectors)
if err != nil {
return "", err
}
sb := sqlbuilder.NewSelectBuilder()
sb.Select(
"trace_id",
"span_id",
"parent_span_id",
"name",
"timestamp",
"duration_nano",
"`service.name`",
fmt.Sprintf("'%s' AS level", cteName),
)
requiredColumns := b.getRequiredAttributeColumns()
for _, col := range requiredColumns {
sb.SelectMore(col)
}
sb.From("base_spans AS s")
if query.Filter != nil && query.Filter.Expression != "" {
filterWhereClause, err := querybuilder.PrepareWhereClause(
query.Filter.Expression,
querybuilder.FilterExprVisitorOpts{
Logger: b.stmtBuilder.logger,
FieldMapper: b.stmtBuilder.fm,
ConditionBuilder: b.stmtBuilder.cb,
FieldKeys: keys,
SkipResourceFilter: true,
},
)
if err != nil {
return "", err
}
if filterWhereClause != nil {
sb.AddWhereClause(filterWhereClause.WhereClause)
}
}
sql, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
b.addCTE(cteName, sql, args, []string{"base_spans"})
return cteName, nil
}
func sanitizeForSQL(s string) string {
replacements := map[string]string{
"=>": "DIRECT_DESC",
"->": "INDIRECT_DESC",
"&&": "AND",
"||": "OR",
"NOT": "NOT",
" ": "_",
}
result := s
for old, new := range replacements {
result = strings.ReplaceAll(result, old, new)
}
return result
}
func (b *traceOperatorCTEBuilder) buildOperatorCTE(op qbtypes.TraceOperatorType, leftCTE, rightCTE string) (string, error) {
sanitizedOp := sanitizeForSQL(op.StringValue())
cteName := fmt.Sprintf("%s_%s_%s", leftCTE, sanitizedOp, rightCTE)
if _, exists := b.cteNameToIndex[cteName]; exists {
return cteName, nil
}
var sql string
var args []any
var dependsOn []string
switch op {
case qbtypes.TraceOperatorDirectDescendant:
sql, args, dependsOn = b.buildDirectDescendantCTE(leftCTE, rightCTE)
case qbtypes.TraceOperatorAnd:
sql, args, dependsOn = b.buildAndCTE(leftCTE, rightCTE)
case qbtypes.TraceOperatorOr:
sql, dependsOn = b.buildOrCTE(leftCTE, rightCTE)
args = nil
case qbtypes.TraceOperatorNot, qbtypes.TraceOperatorExclude:
sql, args, dependsOn = b.buildNotCTE(leftCTE, rightCTE)
default:
return "", errors.NewInvalidInputf(errors.CodeInvalidInput, "unsupported operator: %s", op.StringValue())
}
b.addCTE(cteName, sql, args, dependsOn)
return cteName, nil
}
func (b *traceOperatorCTEBuilder) buildDirectDescendantCTE(parentCTE, childCTE string) (string, []any, []string) {
sb := sqlbuilder.NewSelectBuilder()
sb.Select(
"c.trace_id",
"c.span_id",
"c.parent_span_id",
"c.name",
"c.timestamp",
"c.duration_nano",
"c.`service.name`",
fmt.Sprintf("'%s' AS level", childCTE),
)
requiredColumns := b.getRequiredAttributeColumns()
for _, col := range requiredColumns {
sb.SelectMore(fmt.Sprintf("c.%s", col))
}
sb.From(fmt.Sprintf("%s AS c", childCTE))
sb.JoinWithOption(
sqlbuilder.InnerJoin,
fmt.Sprintf("%s AS p", parentCTE),
"p.trace_id = c.trace_id AND p.span_id = c.parent_span_id",
)
sql, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
return sql, args, []string{parentCTE, childCTE}
}
func (b *traceOperatorCTEBuilder) buildAndCTE(leftCTE, rightCTE string) (string, []any, []string) {
sb := sqlbuilder.NewSelectBuilder()
sb.Select(
"l.trace_id",
"l.span_id",
"l.parent_span_id",
"l.name",
"l.timestamp",
"l.duration_nano",
"l.`service.name`",
"l.level",
)
requiredColumns := b.getRequiredAttributeColumns()
for _, col := range requiredColumns {
sb.SelectMore(fmt.Sprintf("l.%s", col))
}
sb.From(fmt.Sprintf("%s AS l", leftCTE))
sb.JoinWithOption(
sqlbuilder.InnerJoin,
fmt.Sprintf("%s AS r", rightCTE),
"l.trace_id = r.trace_id",
)
sql, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
return sql, args, []string{leftCTE, rightCTE}
}
func (b *traceOperatorCTEBuilder) buildOrCTE(leftCTE, rightCTE string) (string, []string) {
sql := fmt.Sprintf(`
SELECT * FROM %s
UNION DISTINCT
SELECT * FROM %s
`, leftCTE, rightCTE)
return sql, []string{leftCTE, rightCTE}
}
func (b *traceOperatorCTEBuilder) buildNotCTE(leftCTE, rightCTE string) (string, []any, []string) {
sb := sqlbuilder.NewSelectBuilder()
sb.Select(
"l.trace_id",
"l.span_id",
"l.parent_span_id",
"l.name",
"l.timestamp",
"l.duration_nano",
"l.`service.name`",
"l.level",
)
requiredColumns := b.getRequiredAttributeColumns()
for _, col := range requiredColumns {
sb.SelectMore(fmt.Sprintf("l.%s", col))
}
sb.From(fmt.Sprintf("%s AS l", leftCTE))
sb.Where(fmt.Sprintf(
"NOT EXISTS (SELECT 1 FROM %s AS r WHERE r.trace_id = l.trace_id)",
rightCTE,
))
sql, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
return sql, args, []string{leftCTE, rightCTE}
}
func (b *traceOperatorCTEBuilder) buildFinalQuery(selectFromCTE string, requestType qbtypes.RequestType) (*qbtypes.Statement, error) {
switch requestType {
case qbtypes.RequestTypeRaw:
return b.buildListQuery(selectFromCTE)
case qbtypes.RequestTypeTimeSeries:
return b.buildTimeSeriesQuery(selectFromCTE)
case qbtypes.RequestTypeTrace:
return b.buildTraceQuery(selectFromCTE)
case qbtypes.RequestTypeScalar:
return b.buildScalarQuery(selectFromCTE)
default:
return nil, fmt.Errorf("unsupported request type: %s", requestType)
}
}
func (b *traceOperatorCTEBuilder) buildListQuery(selectFromCTE string) (*qbtypes.Statement, error) {
sb := sqlbuilder.NewSelectBuilder()
sb.Select(
"timestamp",
"trace_id",
"span_id",
"name",
"service.name",
"duration_nano",
"parent_span_id",
)
for _, field := range b.operator.SelectFields {
colExpr, err := b.stmtBuilder.fm.ColumnExpressionFor(b.ctx, &field, nil)
if err != nil {
return nil, errors.NewInvalidInputf(
errors.CodeInvalidInput,
"failed to map select field '%s' in list query: %v",
field.Name,
err,
)
}
sb.SelectMore(sqlbuilder.Escape(colExpr))
}
sb.From(selectFromCTE)
// Add order by support
keySelectors := b.getKeySelectors()
keys, _, err := b.stmtBuilder.metadataStore.GetKeysMulti(b.ctx, keySelectors)
if err != nil {
return nil, err
}
orderApplied := false
for _, orderBy := range b.operator.Order {
colExpr, err := b.stmtBuilder.fm.ColumnExpressionFor(b.ctx, &orderBy.Key.TelemetryFieldKey, keys)
if err != nil {
return nil, err
}
sb.OrderBy(fmt.Sprintf("%s %s", colExpr, orderBy.Direction.StringValue()))
orderApplied = true
}
if !orderApplied {
sb.OrderBy("timestamp DESC")
}
if b.operator.Limit > 0 {
sb.Limit(b.operator.Limit)
} else {
sb.Limit(100)
}
if b.operator.Offset > 0 {
sb.Offset(b.operator.Offset)
}
sql, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
return &qbtypes.Statement{
Query: sql,
Args: args,
}, nil
}
func (b *traceOperatorCTEBuilder) getKeySelectors() []*telemetrytypes.FieldKeySelector {
var keySelectors []*telemetrytypes.FieldKeySelector
for _, agg := range b.operator.Aggregations {
selectors := querybuilder.QueryStringToKeysSelectors(agg.Expression)
keySelectors = append(keySelectors, selectors...)
}
if b.operator.Filter != nil && b.operator.Filter.Expression != "" {
selectors := querybuilder.QueryStringToKeysSelectors(b.operator.Filter.Expression)
keySelectors = append(keySelectors, selectors...)
}
for _, gb := range b.operator.GroupBy {
selectors := querybuilder.QueryStringToKeysSelectors(gb.TelemetryFieldKey.Name)
keySelectors = append(keySelectors, selectors...)
}
for _, order := range b.operator.Order {
keySelectors = append(keySelectors, &telemetrytypes.FieldKeySelector{
Name: order.Key.Name,
Signal: telemetrytypes.SignalTraces,
FieldContext: order.Key.FieldContext,
FieldDataType: order.Key.FieldDataType,
})
}
for i := range keySelectors {
keySelectors[i].Signal = telemetrytypes.SignalTraces
}
return keySelectors
}
func (b *traceOperatorCTEBuilder) getRequiredAttributeColumns() []string {
requiredColumns := make(map[string]bool)
allKeySelectors := b.getKeySelectors()
for _, selector := range allKeySelectors {
if b.isIntrinsicField(selector.Name) {
continue
}
if strings.ToLower(selector.Name) == SpanSearchScopeRoot || strings.ToLower(selector.Name) == SpanSearchScopeEntryPoint {
continue
}
switch selector.FieldContext {
case telemetrytypes.FieldContextResource:
requiredColumns["resources_string"] = true
case telemetrytypes.FieldContextAttribute, telemetrytypes.FieldContextSpan, telemetrytypes.FieldContextUnspecified:
switch selector.FieldDataType {
case telemetrytypes.FieldDataTypeString:
requiredColumns["attributes_string"] = true
case telemetrytypes.FieldDataTypeNumber:
requiredColumns["attributes_number"] = true
case telemetrytypes.FieldDataTypeBool:
requiredColumns["attributes_bool"] = true
default:
requiredColumns["attributes_string"] = true
}
}
}
result := make([]string, 0, len(requiredColumns))
for col := range requiredColumns {
result = append(result, col)
}
return result
}
func (b *traceOperatorCTEBuilder) isIntrinsicField(fieldName string) bool {
_, isIntrinsic := IntrinsicFields[fieldName]
if isIntrinsic {
return true
}
_, isIntrinsicDeprecated := IntrinsicFieldsDeprecated[fieldName]
if isIntrinsicDeprecated {
return true
}
_, isCalculated := CalculatedFields[fieldName]
if isCalculated {
return true
}
_, isCalculatedDeprecated := CalculatedFieldsDeprecated[fieldName]
if isCalculatedDeprecated {
return true
}
_, isDefault := DefaultFields[fieldName]
return isDefault
}
func (b *traceOperatorCTEBuilder) buildTimeSeriesQuery(selectFromCTE string) (*qbtypes.Statement, error) {
sb := sqlbuilder.NewSelectBuilder()
stepIntervalSeconds := int64(b.operator.StepInterval.Seconds())
if stepIntervalSeconds <= 0 {
timeRangeSeconds := (b.end - b.start) / querybuilder.NsToSeconds
if timeRangeSeconds > 3600 {
stepIntervalSeconds = 300
} else if timeRangeSeconds > 1800 {
stepIntervalSeconds = 120
} else {
stepIntervalSeconds = 60
}
b.stmtBuilder.logger.WarnContext(b.ctx,
"trace operator stepInterval not set, using default",
"defaultSeconds", stepIntervalSeconds)
}
sb.Select(fmt.Sprintf(
"toStartOfInterval(timestamp, INTERVAL %d SECOND) AS ts",
stepIntervalSeconds,
))
keySelectors := b.getKeySelectors()
keys, _, err := b.stmtBuilder.metadataStore.GetKeysMulti(b.ctx, keySelectors)
if err != nil {
return nil, err
}
var allGroupByArgs []any
for _, gb := range b.operator.GroupBy {
expr, args, err := querybuilder.CollisionHandledFinalExpr(
b.ctx,
&gb.TelemetryFieldKey,
b.stmtBuilder.fm,
b.stmtBuilder.cb,
keys,
telemetrytypes.FieldDataTypeString,
"",
nil,
)
if err != nil {
return nil, errors.NewInvalidInputf(
errors.CodeInvalidInput,
"failed to map group by field '%s': %v",
gb.TelemetryFieldKey.Name,
err,
)
}
colExpr := fmt.Sprintf("toString(%s) AS `%s`", expr, gb.TelemetryFieldKey.Name)
allGroupByArgs = append(allGroupByArgs, args...)
sb.SelectMore(colExpr)
}
var allAggChArgs []any
for i, agg := range b.operator.Aggregations {
rewritten, chArgs, err := b.stmtBuilder.aggExprRewriter.Rewrite(
b.ctx,
agg.Expression,
uint64(stepIntervalSeconds),
keys,
)
if err != nil {
return nil, errors.NewInvalidInputf(
errors.CodeInvalidInput,
"failed to rewrite aggregation expression '%s': %v",
agg.Expression,
err,
)
}
allAggChArgs = append(allAggChArgs, chArgs...)
alias := fmt.Sprintf("__result_%d", i)
sb.SelectMore(fmt.Sprintf("%s AS %s", rewritten, alias))
}
sb.From(selectFromCTE)
sb.GroupBy("ts")
if len(b.operator.GroupBy) > 0 {
groupByKeys := make([]string, len(b.operator.GroupBy))
for i, gb := range b.operator.GroupBy {
groupByKeys[i] = fmt.Sprintf("`%s`", gb.TelemetryFieldKey.Name)
}
sb.GroupBy(groupByKeys...)
}
// Add order by support
for _, orderBy := range b.operator.Order {
idx, ok := b.aggOrderBy(orderBy)
if ok {
sb.OrderBy(fmt.Sprintf("__result_%d %s", idx, orderBy.Direction.StringValue()))
} else {
sb.OrderBy(fmt.Sprintf("`%s` %s", orderBy.Key.Name, orderBy.Direction.StringValue()))
}
}
sb.OrderBy("ts desc")
combinedArgs := append(allGroupByArgs, allAggChArgs...)
// Add HAVING clause if specified
if err := b.addHavingClause(sb); err != nil {
return nil, err
}
// Add limit support
if b.operator.Limit > 0 {
sb.Limit(b.operator.Limit)
}
sql, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse, combinedArgs...)
return &qbtypes.Statement{
Query: sql,
Args: args,
}, nil
}
func (b *traceOperatorCTEBuilder) buildTraceQuery(selectFromCTE string) (*qbtypes.Statement, error) {
sb := sqlbuilder.NewSelectBuilder()
keySelectors := b.getKeySelectors()
keys, _, err := b.stmtBuilder.metadataStore.GetKeysMulti(b.ctx, keySelectors)
if err != nil {
return nil, err
}
var allGroupByArgs []any
for _, gb := range b.operator.GroupBy {
expr, args, err := querybuilder.CollisionHandledFinalExpr(
b.ctx,
&gb.TelemetryFieldKey,
b.stmtBuilder.fm,
b.stmtBuilder.cb,
keys,
telemetrytypes.FieldDataTypeString,
"",
nil,
)
if err != nil {
return nil, errors.NewInvalidInputf(
errors.CodeInvalidInput,
"failed to map group by field '%s': %v",
gb.TelemetryFieldKey.Name,
err,
)
}
colExpr := fmt.Sprintf("toString(%s) AS `%s`", expr, gb.TelemetryFieldKey.Name)
allGroupByArgs = append(allGroupByArgs, args...)
sb.SelectMore(colExpr)
}
rateInterval := (b.end - b.start) / querybuilder.NsToSeconds
var allAggChArgs []any
for i, agg := range b.operator.Aggregations {
rewritten, chArgs, err := b.stmtBuilder.aggExprRewriter.Rewrite(
b.ctx,
agg.Expression,
rateInterval,
keys,
)
if err != nil {
return nil, errors.NewInvalidInputf(
errors.CodeInvalidInput,
"failed to rewrite aggregation expression '%s': %v",
agg.Expression,
err,
)
}
allAggChArgs = append(allAggChArgs, chArgs...)
alias := fmt.Sprintf("__result_%d", i)
sb.SelectMore(fmt.Sprintf("%s AS %s", rewritten, alias))
}
traceSubquery := fmt.Sprintf("SELECT DISTINCT trace_id FROM %s", selectFromCTE)
sb.Select(
"any(timestamp) as timestamp",
"any(`service.name`) as `service.name`",
"any(name) as `name`",
"count() as span_count",
"any(duration_nano) as `duration_nano`",
"trace_id as `trace_id`",
)
sb.From("base_spans")
sb.Where(
fmt.Sprintf("trace_id GLOBAL IN (%s)", traceSubquery),
"parent_span_id = ''",
)
sb.GroupBy("trace_id")
if len(b.operator.GroupBy) > 0 {
groupByKeys := make([]string, len(b.operator.GroupBy))
for i, gb := range b.operator.GroupBy {
groupByKeys[i] = fmt.Sprintf("`%s`", gb.TelemetryFieldKey.Name)
}
sb.GroupBy(groupByKeys...)
}
// Add HAVING clause if specified
if err := b.addHavingClause(sb); err != nil {
return nil, err
}
orderApplied := false
for _, orderBy := range b.operator.Order {
switch orderBy.Key.Name {
case qbtypes.OrderByTraceDuration.StringValue():
sb.OrderBy(fmt.Sprintf("`duration_nano` %s", orderBy.Direction.StringValue()))
orderApplied = true
case qbtypes.OrderBySpanCount.StringValue():
sb.OrderBy(fmt.Sprintf("span_count %s", orderBy.Direction.StringValue()))
orderApplied = true
case "timestamp":
sb.OrderBy(fmt.Sprintf("timestamp %s", orderBy.Direction.StringValue()))
orderApplied = true
default:
aggIndex := -1
for i, agg := range b.operator.Aggregations {
if orderBy.Key.Name == agg.Alias || orderBy.Key.Name == fmt.Sprintf("__result_%d", i) {
aggIndex = i
break
}
}
if aggIndex >= 0 {
alias := fmt.Sprintf("__result_%d", aggIndex)
if b.operator.Aggregations[aggIndex].Alias != "" {
alias = b.operator.Aggregations[aggIndex].Alias
}
sb.OrderBy(fmt.Sprintf("%s %s", alias, orderBy.Direction.StringValue()))
orderApplied = true
} else {
b.stmtBuilder.logger.WarnContext(b.ctx,
"ignoring order by field that's not available in trace context",
"field", orderBy.Key.Name)
}
}
}
if !orderApplied {
sb.OrderBy("`duration_nano` DESC")
}
if b.operator.Limit > 0 {
sb.Limit(b.operator.Limit)
}
combinedArgs := append(allGroupByArgs, allAggChArgs...)
sql, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse, combinedArgs...)
return &qbtypes.Statement{
Query: sql,
Args: args,
}, nil
}
func (b *traceOperatorCTEBuilder) buildScalarQuery(selectFromCTE string) (*qbtypes.Statement, error) {
sb := sqlbuilder.NewSelectBuilder()
keySelectors := b.getKeySelectors()
keys, _, err := b.stmtBuilder.metadataStore.GetKeysMulti(b.ctx, keySelectors)
if err != nil {
return nil, err
}
var allGroupByArgs []any
for _, gb := range b.operator.GroupBy {
expr, args, err := querybuilder.CollisionHandledFinalExpr(
b.ctx,
&gb.TelemetryFieldKey,
b.stmtBuilder.fm,
b.stmtBuilder.cb,
keys,
telemetrytypes.FieldDataTypeString,
"",
nil,
)
if err != nil {
return nil, errors.NewInvalidInputf(
errors.CodeInvalidInput,
"failed to map group by field '%s': %v",
gb.TelemetryFieldKey.Name,
err,
)
}
colExpr := fmt.Sprintf("toString(%s) AS `%s`", expr, gb.TelemetryFieldKey.Name)
allGroupByArgs = append(allGroupByArgs, args...)
sb.SelectMore(colExpr)
}
var allAggChArgs []any
for i, agg := range b.operator.Aggregations {
rewritten, chArgs, err := b.stmtBuilder.aggExprRewriter.Rewrite(
b.ctx,
agg.Expression,
uint64((b.end-b.start)/querybuilder.NsToSeconds),
keys,
)
if err != nil {
return nil, errors.NewInvalidInputf(
errors.CodeInvalidInput,
"failed to rewrite aggregation expression '%s': %v",
agg.Expression,
err,
)
}
allAggChArgs = append(allAggChArgs, chArgs...)
alias := fmt.Sprintf("__result_%d", i)
sb.SelectMore(fmt.Sprintf("%s AS %s", rewritten, alias))
}
sb.From(selectFromCTE)
if len(b.operator.GroupBy) > 0 {
groupByKeys := make([]string, len(b.operator.GroupBy))
for i, gb := range b.operator.GroupBy {
groupByKeys[i] = fmt.Sprintf("`%s`", gb.TelemetryFieldKey.Name)
}
sb.GroupBy(groupByKeys...)
}
// Add order by support
for _, orderBy := range b.operator.Order {
idx, ok := b.aggOrderBy(orderBy)
if ok {
sb.OrderBy(fmt.Sprintf("__result_%d %s", idx, orderBy.Direction.StringValue()))
} else {
sb.OrderBy(fmt.Sprintf("`%s` %s", orderBy.Key.Name, orderBy.Direction.StringValue()))
}
}
// Add default ordering if no orderBy specified
if len(b.operator.Order) == 0 {
sb.OrderBy("__result_0 DESC")
}
// Add limit support
if b.operator.Limit > 0 {
sb.Limit(b.operator.Limit)
}
combinedArgs := append(allGroupByArgs, allAggChArgs...)
// Add HAVING clause if specified
if err := b.addHavingClause(sb); err != nil {
return nil, err
}
sql, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse, combinedArgs...)
return &qbtypes.Statement{
Query: sql,
Args: args,
}, nil
}
func (b *traceOperatorCTEBuilder) addHavingClause(sb *sqlbuilder.SelectBuilder) error {
if b.operator.Having != nil && b.operator.Having.Expression != "" {
rewriter := querybuilder.NewHavingExpressionRewriter()
rewrittenExpr := rewriter.RewriteForTraces(b.operator.Having.Expression, b.operator.Aggregations)
sb.Having(rewrittenExpr)
}
return nil
}
func (b *traceOperatorCTEBuilder) addCTE(name, sql string, args []any, dependsOn []string) {
b.ctes = append(b.ctes, cteNode{
name: name,
sql: sql,
args: args,
dependsOn: dependsOn,
})
b.cteNameToIndex[name] = len(b.ctes) - 1
}
func (b *traceOperatorCTEBuilder) aggOrderBy(k qbtypes.OrderBy) (int, bool) {
for i, agg := range b.operator.Aggregations {
if k.Key.Name == agg.Alias ||
k.Key.Name == agg.Expression ||
k.Key.Name == fmt.Sprintf("__result_%d", i) {
return i, true
}
}
return 0, false
}

View File

@@ -0,0 +1,89 @@
package telemetrytraces
import (
"context"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/querybuilder"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"log/slog"
)
type traceOperatorStatementBuilder struct {
logger *slog.Logger
metadataStore telemetrytypes.MetadataStore
fm qbtypes.FieldMapper
cb qbtypes.ConditionBuilder
traceStmtBuilder qbtypes.StatementBuilder[qbtypes.TraceAggregation]
aggExprRewriter qbtypes.AggExprRewriter
}
var _ qbtypes.TraceOperatorStatementBuilder = (*traceOperatorStatementBuilder)(nil)
func NewTraceOperatorStatementBuilder(
settings factory.ProviderSettings,
metadataStore telemetrytypes.MetadataStore,
fieldMapper qbtypes.FieldMapper,
conditionBuilder qbtypes.ConditionBuilder,
traceStmtBuilder qbtypes.StatementBuilder[qbtypes.TraceAggregation],
aggExprRewriter qbtypes.AggExprRewriter,
) *traceOperatorStatementBuilder {
tracesSettings := factory.NewScopedProviderSettings(settings, "github.com/SigNoz/signoz/pkg/telemetrytraces")
return &traceOperatorStatementBuilder{
logger: tracesSettings.Logger(),
metadataStore: metadataStore,
fm: fieldMapper,
cb: conditionBuilder,
traceStmtBuilder: traceStmtBuilder,
aggExprRewriter: aggExprRewriter,
}
}
// Build builds a SQL query based on the given parameters.
func (b *traceOperatorStatementBuilder) Build(
ctx context.Context,
start uint64,
end uint64,
requestType qbtypes.RequestType,
query qbtypes.QueryBuilderTraceOperator,
compositeQuery *qbtypes.CompositeQuery,
) (*qbtypes.Statement, error) {
start = querybuilder.ToNanoSecs(start)
end = querybuilder.ToNanoSecs(end)
// Parse the expression if not already parsed
if query.ParsedExpression == nil {
if err := query.ParseExpression(); err != nil {
return nil, err
}
}
// Validate compositeQuery parameter
if compositeQuery == nil {
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "compositeQuery cannot be nil")
}
// Build the CTE-based query
builder := &traceOperatorCTEBuilder{
ctx: ctx,
start: start,
end: end,
operator: &query,
stmtBuilder: b,
queries: make(map[string]*qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]),
ctes: []cteNode{}, // Use slice to maintain order
cteNameToIndex: make(map[string]int),
queryToCTEName: make(map[string]string),
compositeQuery: compositeQuery, // Now passed as explicit parameter
}
// Collect all referenced queries
if err := builder.collectQueries(); err != nil {
return nil, err
}
// Build the query
return builder.build(requestType)
}

Some files were not shown because too many files have changed in this diff Show More