From 8b21ba5db9eb2b80ad603da370c3ae6f10ee6d04 Mon Sep 17 00:00:00 2001 From: Abhi kumar Date: Mon, 29 Sep 2025 19:12:50 +0530 Subject: [PATCH] ISSUE:2806 - View traces/logs functionality across the product with new QB (#9207) * fix: issue-2806 view traces/logs functionality across the product with new qb * test: added test for getfilter * test: updated tests --- .../prepareQueryRangePayloadV5.test.ts | 256 ++++++++++++++++++ .../queryRange/prepareQueryRangePayloadV5.ts | 21 +- .../CeleryTask/useNavigateToExplorer.ts | 37 ++- 3 files changed, 301 insertions(+), 13 deletions(-) diff --git a/frontend/src/api/v5/queryRange/prepareQueryRangePayloadV5.test.ts b/frontend/src/api/v5/queryRange/prepareQueryRangePayloadV5.test.ts index 952ee1c09125..d38452993981 100644 --- a/frontend/src/api/v5/queryRange/prepareQueryRangePayloadV5.test.ts +++ b/frontend/src/api/v5/queryRange/prepareQueryRangePayloadV5.test.ts @@ -634,4 +634,260 @@ describe('prepareQueryRangePayloadV5', () => { }), ); }); + + it('builds payload for builder queries with filters array but no filter expression', () => { + const props: GetQueryResultsProps = { + query: { + queryType: EQueryType.QUERY_BUILDER, + id: 'q8', + unit: undefined, + promql: [], + clickhouse_sql: [], + builder: { + queryData: [ + baseBuilderQuery({ + dataSource: DataSource.LOGS, + filter: { expression: '' }, + filters: { + items: [ + { + id: '1', + key: { key: 'service.name', type: 'string' }, + op: '=', + value: 'payment-service', + }, + { + id: '2', + key: { key: 'http.status_code', type: 'number' }, + op: '>=', + value: 400, + }, + { + id: '3', + key: { key: 'message', type: 'string' }, + op: 'contains', + value: 'error', + }, + ], + op: 'AND', + }, + }), + ], + queryFormulas: [], + queryTraceOperator: [], + }, + }, + graphType: PANEL_TYPES.LIST, + selectedTime: 'GLOBAL_TIME', + start, + end, + }; + + const result = prepareQueryRangePayloadV5(props); + + expect(result.legendMap).toEqual({ A: 'Legend A' }); + expect(result.queryPayload.compositeQuery.queries).toHaveLength(1); + + const builderQuery = result.queryPayload.compositeQuery.queries.find( + (q) => q.type === 'builder_query', + ) as QueryEnvelope; + const logSpec = builderQuery.spec as LogBuilderQuery; + + expect(logSpec.name).toBe('A'); + expect(logSpec.signal).toBe('logs'); + expect(logSpec.filter).toEqual({ + expression: + "service.name = 'payment-service' AND http.status_code >= 400 AND message contains 'error'", + }); + }); + + it('uses filter.expression when only expression is provided', () => { + const props: GetQueryResultsProps = { + query: { + queryType: EQueryType.QUERY_BUILDER, + id: 'q9', + unit: undefined, + promql: [], + clickhouse_sql: [], + builder: { + queryData: [ + baseBuilderQuery({ + dataSource: DataSource.LOGS, + filter: { expression: 'http.status_code >= 500' }, + filters: (undefined as unknown) as IBuilderQuery['filters'], + }), + ], + queryFormulas: [], + queryTraceOperator: [], + }, + }, + graphType: PANEL_TYPES.LIST, + selectedTime: 'GLOBAL_TIME', + start, + end, + }; + + const result = prepareQueryRangePayloadV5(props); + const builderQuery = result.queryPayload.compositeQuery.queries.find( + (q) => q.type === 'builder_query', + ) as QueryEnvelope; + const logSpec = builderQuery.spec as LogBuilderQuery; + expect(logSpec.filter).toEqual({ expression: 'http.status_code >= 500' }); + }); + + it('derives expression from filters when filter is undefined', () => { + const props: GetQueryResultsProps = { + query: { + queryType: EQueryType.QUERY_BUILDER, + id: 'q10', + unit: undefined, + promql: [], + clickhouse_sql: [], + builder: { + queryData: [ + baseBuilderQuery({ + dataSource: DataSource.LOGS, + filter: (undefined as unknown) as IBuilderQuery['filter'], + filters: { + items: [ + { + id: '1', + key: { key: 'service.name', type: 'string' }, + op: '=', + value: 'checkout', + }, + ], + op: 'AND', + }, + }), + ], + queryFormulas: [], + queryTraceOperator: [], + }, + }, + graphType: PANEL_TYPES.LIST, + selectedTime: 'GLOBAL_TIME', + start, + end, + }; + + const result = prepareQueryRangePayloadV5(props); + const builderQuery = result.queryPayload.compositeQuery.queries.find( + (q) => q.type === 'builder_query', + ) as QueryEnvelope; + const logSpec = builderQuery.spec as LogBuilderQuery; + expect(logSpec.filter).toEqual({ expression: "service.name = 'checkout'" }); + }); + + it('prefers filter.expression over filters when both are present', () => { + const props: GetQueryResultsProps = { + query: { + queryType: EQueryType.QUERY_BUILDER, + id: 'q11', + unit: undefined, + promql: [], + clickhouse_sql: [], + builder: { + queryData: [ + baseBuilderQuery({ + dataSource: DataSource.LOGS, + filter: { expression: "service.name = 'frontend'" }, + filters: { + items: [ + { + id: '1', + key: { key: 'service.name', type: 'string' }, + op: '=', + value: 'backend', + }, + ], + op: 'AND', + }, + }), + ], + queryFormulas: [], + queryTraceOperator: [], + }, + }, + graphType: PANEL_TYPES.LIST, + selectedTime: 'GLOBAL_TIME', + start, + end, + }; + + const result = prepareQueryRangePayloadV5(props); + const builderQuery = result.queryPayload.compositeQuery.queries.find( + (q) => q.type === 'builder_query', + ) as QueryEnvelope; + const logSpec = builderQuery.spec as LogBuilderQuery; + expect(logSpec.filter).toEqual({ expression: "service.name = 'frontend'" }); + }); + + it('returns empty expression when neither filter nor filters provided', () => { + const props: GetQueryResultsProps = { + query: { + queryType: EQueryType.QUERY_BUILDER, + id: 'q12', + unit: undefined, + promql: [], + clickhouse_sql: [], + builder: { + queryData: [ + baseBuilderQuery({ + dataSource: DataSource.LOGS, + filter: (undefined as unknown) as IBuilderQuery['filter'], + filters: (undefined as unknown) as IBuilderQuery['filters'], + }), + ], + queryFormulas: [], + queryTraceOperator: [], + }, + }, + graphType: PANEL_TYPES.LIST, + selectedTime: 'GLOBAL_TIME', + start, + end, + }; + + const result = prepareQueryRangePayloadV5(props); + const builderQuery = result.queryPayload.compositeQuery.queries.find( + (q) => q.type === 'builder_query', + ) as QueryEnvelope; + const logSpec = builderQuery.spec as LogBuilderQuery; + expect(logSpec.filter).toEqual({ expression: '' }); + }); + + it('returns empty expression when filters provided with empty items', () => { + const props: GetQueryResultsProps = { + query: { + queryType: EQueryType.QUERY_BUILDER, + id: 'q13', + unit: undefined, + promql: [], + clickhouse_sql: [], + builder: { + queryData: [ + baseBuilderQuery({ + dataSource: DataSource.LOGS, + filter: { expression: '' }, + filters: { items: [], op: 'AND' }, + }), + ], + queryFormulas: [], + queryTraceOperator: [], + }, + }, + graphType: PANEL_TYPES.LIST, + selectedTime: 'GLOBAL_TIME', + start, + end, + }; + + const result = prepareQueryRangePayloadV5(props); + const builderQuery = result.queryPayload.compositeQuery.queries.find( + (q) => q.type === 'builder_query', + ) as QueryEnvelope; + const logSpec = builderQuery.spec as LogBuilderQuery; + expect(logSpec.filter).toEqual({ expression: '' }); + }); }); diff --git a/frontend/src/api/v5/queryRange/prepareQueryRangePayloadV5.ts b/frontend/src/api/v5/queryRange/prepareQueryRangePayloadV5.ts index ba4f8f3f8225..d30051716fff 100644 --- a/frontend/src/api/v5/queryRange/prepareQueryRangePayloadV5.ts +++ b/frontend/src/api/v5/queryRange/prepareQueryRangePayloadV5.ts @@ -1,5 +1,6 @@ /* eslint-disable sonarjs/cognitive-complexity */ /* eslint-disable sonarjs/no-identical-functions */ +import { convertFiltersToExpression } from 'components/QueryBuilderV2/utils'; import { PANEL_TYPES } from 'constants/queryBuilder'; import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults'; import getStartEndRangeTime from 'lib/getStartEndRangeTime'; @@ -14,6 +15,7 @@ import { BaseBuilderQuery, FieldContext, FieldDataType, + Filter, FunctionName, GroupByKey, Having, @@ -111,6 +113,23 @@ function isDeprecatedField(fieldName: string): boolean { ); } +function getFilter(queryData: IBuilderQuery): Filter { + const { filter } = queryData; + if (filter?.expression) { + return { + expression: filter.expression, + }; + } + + if (queryData.filters && queryData.filters?.items?.length > 0) { + return convertFiltersToExpression(queryData.filters); + } + + return { + expression: '', + }; +} + function createBaseSpec( queryData: IBuilderQuery, requestType: RequestType, @@ -124,7 +143,7 @@ function createBaseSpec( return { stepInterval: queryData?.stepInterval || null, disabled: queryData.disabled, - filter: queryData?.filter?.expression ? queryData.filter : undefined, + filter: getFilter(queryData), groupBy: queryData.groupBy?.length > 0 ? queryData.groupBy.map( diff --git a/frontend/src/components/CeleryTask/useNavigateToExplorer.ts b/frontend/src/components/CeleryTask/useNavigateToExplorer.ts index ea6b204afe40..bae077887d66 100644 --- a/frontend/src/components/CeleryTask/useNavigateToExplorer.ts +++ b/frontend/src/components/CeleryTask/useNavigateToExplorer.ts @@ -42,18 +42,31 @@ export function useNavigateToExplorer(): ( builder: { ...widgetQuery.builder, queryData: widgetQuery.builder.queryData - .map((item) => ({ - ...item, - dataSource, - aggregateOperator: MetricAggregateOperator.NOOP, - filters: { - ...item.filters, - items: [...(item.filters?.items || []), ...selectedFilters], - op: item.filters?.op || 'AND', - }, - groupBy: [], - disabled: false, - })) + .map((item) => { + // filter out filters with unique ids + const seen = new Set(); + const filterItems = [ + ...(item.filters?.items || []), + ...selectedFilters, + ].filter((item) => { + if (seen.has(item.id)) return false; + seen.add(item.id); + return true; + }); + + return { + ...item, + dataSource, + aggregateOperator: MetricAggregateOperator.NOOP, + filters: { + ...item.filters, + items: filterItems, + op: item.filters?.op || 'AND', + }, + groupBy: [], + disabled: false, + }; + }) .slice(0, 1), queryFormulas: [], },