mirror of
https://github.com/SigNoz/signoz.git
synced 2025-12-29 08:00:59 +00:00
Compare commits
3 Commits
issue/8880
...
fix/qb-lim
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f21c252fc9 | ||
|
|
53bf3cb50f | ||
|
|
f4b74eef11 |
@@ -50,6 +50,13 @@
|
||||
color: var(--bg-vanilla-400) !important;
|
||||
font-size: 12px !important;
|
||||
}
|
||||
&[type='number']::-webkit-inner-spin-button,
|
||||
&[type='number']::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
|
||||
@@ -375,6 +375,7 @@ function QueryAddOns({
|
||||
<div className="add-on-content" data-testid="limit-content">
|
||||
<InputWithLabel
|
||||
label="Limit"
|
||||
type="number"
|
||||
onChange={handleChangeLimit}
|
||||
initialValue={query?.limit ?? undefined}
|
||||
placeholder="Enter limit"
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
/* eslint-disable react/display-name */
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
import { jest } from '@jest/globals';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||
import { render, screen } from 'tests/test-utils';
|
||||
import { Having, IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { UseQueryOperations } from 'types/common/operations.types';
|
||||
import { DataSource, QueryBuilderContextType } from 'types/common/queryBuilder';
|
||||
|
||||
import { QueryV2 } from '../QueryV2';
|
||||
|
||||
// Local mocks for domain-specific heavy child components
|
||||
jest.mock(
|
||||
'../QueryAggregation/QueryAggregation',
|
||||
() =>
|
||||
function () {
|
||||
return <div>QueryAggregation</div>;
|
||||
},
|
||||
);
|
||||
jest.mock(
|
||||
'../MerticsAggregateSection/MetricsAggregateSection',
|
||||
() =>
|
||||
function () {
|
||||
return <div>MetricsAggregateSection</div>;
|
||||
},
|
||||
);
|
||||
// Mock hooks
|
||||
jest.mock('hooks/queryBuilder/useQueryBuilder');
|
||||
jest.mock('hooks/queryBuilder/useQueryBuilderOperations');
|
||||
|
||||
const mockedUseQueryBuilder = jest.mocked(useQueryBuilder);
|
||||
const mockedUseQueryOperations = jest.mocked(
|
||||
useQueryOperations,
|
||||
) as jest.MockedFunction<UseQueryOperations>;
|
||||
|
||||
describe('QueryV2 - base render', () => {
|
||||
beforeEach(() => {
|
||||
const mockCloneQuery = jest.fn() as jest.MockedFunction<
|
||||
(type: string, q: IBuilderQuery) => void
|
||||
>;
|
||||
|
||||
mockedUseQueryBuilder.mockReturnValue(({
|
||||
// Only fields used by QueryV2
|
||||
cloneQuery: mockCloneQuery,
|
||||
panelType: null,
|
||||
} as unknown) as QueryBuilderContextType);
|
||||
|
||||
mockedUseQueryOperations.mockReturnValue({
|
||||
isTracePanelType: false,
|
||||
isMetricsDataSource: false,
|
||||
operators: [],
|
||||
spaceAggregationOptions: [],
|
||||
listOfAdditionalFilters: [],
|
||||
handleChangeOperator: jest.fn(),
|
||||
handleSpaceAggregationChange: jest.fn(),
|
||||
handleChangeAggregatorAttribute: jest.fn(),
|
||||
handleChangeDataSource: jest.fn(),
|
||||
handleDeleteQuery: jest.fn(),
|
||||
handleChangeQueryData: (jest.fn() as unknown) as ReturnType<UseQueryOperations>['handleChangeQueryData'],
|
||||
handleChangeFormulaData: jest.fn(),
|
||||
handleQueryFunctionsUpdates: jest.fn(),
|
||||
listOfAdditionalFormulaFilters: [],
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders limit input when dataSource is logs', () => {
|
||||
const baseQuery: IBuilderQuery = {
|
||||
queryName: 'A',
|
||||
dataSource: DataSource.LOGS,
|
||||
aggregateOperator: '',
|
||||
aggregations: [],
|
||||
timeAggregation: '',
|
||||
spaceAggregation: '',
|
||||
temporality: '',
|
||||
functions: [],
|
||||
filter: undefined,
|
||||
filters: { items: [], op: 'AND' },
|
||||
groupBy: [],
|
||||
expression: '',
|
||||
disabled: false,
|
||||
having: [] as Having[],
|
||||
limit: 10,
|
||||
stepInterval: null,
|
||||
orderBy: [],
|
||||
legend: 'A',
|
||||
};
|
||||
|
||||
render(
|
||||
<QueryV2
|
||||
index={0}
|
||||
isAvailableToDisable
|
||||
query={baseQuery}
|
||||
version="v4"
|
||||
onSignalSourceChange={jest.fn() as jest.MockedFunction<(v: string) => void>}
|
||||
signalSourceChangeEnabled={false}
|
||||
queriesCount={1}
|
||||
showTraceOperator={false}
|
||||
hasTraceOperator={false}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Ensure the Limit add-on input is present and is of type number
|
||||
const limitInput = screen.getByPlaceholderText(
|
||||
'Enter limit',
|
||||
) as HTMLInputElement;
|
||||
expect(limitInput).toBeInTheDocument();
|
||||
expect(limitInput).toHaveAttribute('type', 'number');
|
||||
expect(limitInput).toHaveAttribute('name', 'limit');
|
||||
expect(limitInput).toHaveAttribute('data-testid', 'input-Limit');
|
||||
});
|
||||
});
|
||||
@@ -242,6 +242,7 @@ export function Formula({
|
||||
</div>
|
||||
<InputWithLabel
|
||||
label="Limit"
|
||||
type="number"
|
||||
onChange={(value): void => handleChangeLimit(Number(value))}
|
||||
initialValue={formula?.limit ?? undefined}
|
||||
placeholder="Enter limit"
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
getViewQuery,
|
||||
isValidQueryName,
|
||||
} from '../drilldownUtils';
|
||||
import { METRIC_TO_LOGS_TRACES_MAPPINGS } from '../metricsCorrelationUtils';
|
||||
|
||||
// Mock the transformMetricsToLogsTraces function since it's not exported
|
||||
// We'll test it indirectly through getViewQuery
|
||||
@@ -161,16 +162,16 @@ describe('drilldownUtils', () => {
|
||||
// Verify transformations were applied
|
||||
if (filterExpression) {
|
||||
// Rule 2: operation → name
|
||||
expect(filterExpression).toContain('name = "GET"');
|
||||
expect(filterExpression).not.toContain('operation = "GET"');
|
||||
expect(filterExpression).toContain(`name = 'GET'`);
|
||||
expect(filterExpression).not.toContain(`operation = 'GET'`);
|
||||
|
||||
// Rule 3: span.kind → kind
|
||||
expect(filterExpression).toContain('kind = 2');
|
||||
expect(filterExpression).not.toContain('span.kind = SPAN_KIND_SERVER');
|
||||
expect(filterExpression).toContain(`kind = '2'`);
|
||||
expect(filterExpression).not.toContain(`span.kind = SPAN_KIND_SERVER`);
|
||||
|
||||
// Rule 4: status.code → status_code_string with value mapping
|
||||
expect(filterExpression).toContain('status_code_string = Ok');
|
||||
expect(filterExpression).not.toContain('status.code = STATUS_CODE_OK');
|
||||
expect(filterExpression).toContain(`status_code_string = 'Ok'`);
|
||||
expect(filterExpression).not.toContain(`status.code = STATUS_CODE_OK`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -192,40 +193,16 @@ describe('drilldownUtils', () => {
|
||||
// Verify transformations were applied
|
||||
if (filterExpression) {
|
||||
// Rule 2: operation → name
|
||||
expect(filterExpression).toContain('name = "GET"');
|
||||
expect(filterExpression).not.toContain('operation = "GET"');
|
||||
expect(filterExpression).toContain(`name = 'GET'`);
|
||||
expect(filterExpression).not.toContain(`operation = 'GET'`);
|
||||
|
||||
// Rule 3: span.kind → kind
|
||||
expect(filterExpression).toContain('kind = 2');
|
||||
expect(filterExpression).not.toContain('span.kind = SPAN_KIND_SERVER');
|
||||
expect(filterExpression).toContain(`kind = '2'`);
|
||||
expect(filterExpression).not.toContain(`span.kind = SPAN_KIND_SERVER`);
|
||||
|
||||
// Rule 4: status.code → status_code_string with value mapping
|
||||
expect(filterExpression).toContain('status_code_string = Ok');
|
||||
expect(filterExpression).not.toContain('status.code = STATUS_CODE_OK');
|
||||
}
|
||||
});
|
||||
|
||||
it('should NOT transform metrics query when drilling down to metrics', () => {
|
||||
const result = getViewQuery(
|
||||
mockMetricsQuery,
|
||||
mockFilters,
|
||||
'view_metrics',
|
||||
'metrics_query',
|
||||
);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.builder.queryData).toHaveLength(1);
|
||||
|
||||
// Check that the filter expression was NOT transformed
|
||||
const filterExpression = result?.builder.queryData[0]?.filter?.expression;
|
||||
expect(filterExpression).toBeDefined();
|
||||
|
||||
// Verify NO transformations were applied
|
||||
if (filterExpression) {
|
||||
// Should still contain original metric format
|
||||
expect(filterExpression).toContain('operation = "GET"');
|
||||
expect(filterExpression).toContain('span.kind = SPAN_KIND_SERVER');
|
||||
expect(filterExpression).toContain('status.code = STATUS_CODE_OK');
|
||||
expect(filterExpression).toContain(`status_code_string = 'Ok'`);
|
||||
expect(filterExpression).not.toContain(`status.code = STATUS_CODE_OK`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -258,10 +235,10 @@ describe('drilldownUtils', () => {
|
||||
|
||||
if (filterExpression) {
|
||||
// All transformations should be applied
|
||||
expect(filterExpression).toContain('name = "POST"');
|
||||
expect(filterExpression).toContain('kind = 3');
|
||||
expect(filterExpression).toContain('status_code_string = Error');
|
||||
expect(filterExpression).toContain('http.status_code = 500');
|
||||
expect(filterExpression).toContain(`name = 'POST'`);
|
||||
expect(filterExpression).toContain(`kind = '3'`);
|
||||
expect(filterExpression).toContain(`status_code_string = 'Error'`);
|
||||
expect(filterExpression).toContain(`http.status_code = 500`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -299,13 +276,12 @@ describe('drilldownUtils', () => {
|
||||
});
|
||||
|
||||
it('should handle all status code value mappings correctly', () => {
|
||||
const statusCodeTests = [
|
||||
{ input: 'STATUS_CODE_UNSET', expected: 'Unset' },
|
||||
{ input: 'STATUS_CODE_OK', expected: 'Ok' },
|
||||
{ input: 'STATUS_CODE_ERROR', expected: 'Error' },
|
||||
];
|
||||
const mappingsByAttr = Object.fromEntries(
|
||||
METRIC_TO_LOGS_TRACES_MAPPINGS.map((m) => [m.attribute, m]),
|
||||
) as Record<string, { valueMappings: Record<string, string> }>;
|
||||
const statusMap = mappingsByAttr['status.code'].valueMappings;
|
||||
|
||||
statusCodeTests.forEach(({ input, expected }) => {
|
||||
Object.entries(statusMap).forEach(([input, expected]) => {
|
||||
const testQuery: Query = {
|
||||
...mockMetricsQuery,
|
||||
builder: {
|
||||
@@ -329,19 +305,18 @@ describe('drilldownUtils', () => {
|
||||
);
|
||||
const filterExpression = result?.builder.queryData[0]?.filter?.expression;
|
||||
|
||||
expect(filterExpression).toContain(`status_code_string = ${expected}`);
|
||||
expect(filterExpression).toContain(`status_code_string = '${expected}'`);
|
||||
expect(filterExpression).not.toContain(`status.code = ${input}`);
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle quoted status code values (browser scenario)', () => {
|
||||
const statusCodeTests = [
|
||||
{ input: '"STATUS_CODE_UNSET"', expected: '"Unset"' },
|
||||
{ input: '"STATUS_CODE_OK"', expected: '"Ok"' },
|
||||
{ input: '"STATUS_CODE_ERROR"', expected: '"Error"' },
|
||||
];
|
||||
const mappingsByAttr = Object.fromEntries(
|
||||
METRIC_TO_LOGS_TRACES_MAPPINGS.map((m) => [m.attribute, m]),
|
||||
) as Record<string, { valueMappings: Record<string, string> }>;
|
||||
const statusMap = mappingsByAttr['status.code'].valueMappings;
|
||||
|
||||
statusCodeTests.forEach(({ input, expected }) => {
|
||||
Object.entries(statusMap).forEach(([input, expected]) => {
|
||||
const testQuery: Query = {
|
||||
...mockMetricsQuery,
|
||||
builder: {
|
||||
@@ -350,7 +325,7 @@ describe('drilldownUtils', () => {
|
||||
{
|
||||
...mockMetricsQuery.builder.queryData[0],
|
||||
filter: {
|
||||
expression: `status.code = ${input}`,
|
||||
expression: `status.code = "${input}"`,
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -366,8 +341,8 @@ describe('drilldownUtils', () => {
|
||||
const filterExpression = result?.builder.queryData[0]?.filter?.expression;
|
||||
|
||||
// Should preserve the quoting from the original expression
|
||||
expect(filterExpression).toContain(`status_code_string = ${expected}`);
|
||||
expect(filterExpression).not.toContain(`status.code = ${input}`);
|
||||
expect(filterExpression).toContain(`status_code_string = '${expected}'`);
|
||||
expect(filterExpression).not.toContain(`status.code = "${input}"`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -398,8 +373,8 @@ describe('drilldownUtils', () => {
|
||||
|
||||
if (filterExpression) {
|
||||
// Transformed attributes
|
||||
expect(filterExpression).toContain('name = "GET"');
|
||||
expect(filterExpression).toContain('kind = 2');
|
||||
expect(filterExpression).toContain(`name = 'GET'`);
|
||||
expect(filterExpression).toContain(`kind = '2'`);
|
||||
|
||||
// Preserved non-metric attributes
|
||||
expect(filterExpression).toContain('service = "test-service"');
|
||||
@@ -408,15 +383,12 @@ describe('drilldownUtils', () => {
|
||||
});
|
||||
|
||||
it('should handle all span.kind value mappings correctly', () => {
|
||||
const spanKindTests = [
|
||||
{ input: 'SPAN_KIND_INTERNAL', expected: '1' },
|
||||
{ input: 'SPAN_KIND_CONSUMER', expected: '5' },
|
||||
{ input: 'SPAN_KIND_CLIENT', expected: '3' },
|
||||
{ input: 'SPAN_KIND_PRODUCER', expected: '4' },
|
||||
{ input: 'SPAN_KIND_SERVER', expected: '2' },
|
||||
];
|
||||
const mappingsByAttr = Object.fromEntries(
|
||||
METRIC_TO_LOGS_TRACES_MAPPINGS.map((m) => [m.attribute, m]),
|
||||
) as Record<string, { valueMappings: Record<string, string> }>;
|
||||
const spanKindMap = mappingsByAttr['span.kind'].valueMappings;
|
||||
|
||||
spanKindTests.forEach(({ input, expected }) => {
|
||||
Object.entries(spanKindMap).forEach(([input, expected]) => {
|
||||
const testQuery: Query = {
|
||||
...mockMetricsQuery,
|
||||
builder: {
|
||||
@@ -440,9 +412,48 @@ describe('drilldownUtils', () => {
|
||||
);
|
||||
const filterExpression = result?.builder.queryData[0]?.filter?.expression;
|
||||
|
||||
expect(filterExpression).toContain(`kind = ${expected}`);
|
||||
expect(filterExpression).toContain(`kind = '${expected}'`);
|
||||
expect(filterExpression).not.toContain(`span.kind = ${input}`);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not transform when the source query is not metrics (logs/traces sources)', () => {
|
||||
(['logs', 'traces'] as const).forEach((source) => {
|
||||
const nonMetricsQuery: Query = {
|
||||
...mockMetricsQuery,
|
||||
builder: {
|
||||
...mockMetricsQuery.builder,
|
||||
queryData: [
|
||||
{
|
||||
...mockMetricsQuery.builder.queryData[0],
|
||||
dataSource: source as any,
|
||||
filter: {
|
||||
expression:
|
||||
'operation = "GET" AND span.kind = SPAN_KIND_SERVER AND status.code = STATUS_CODE_OK',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const result = getViewQuery(
|
||||
nonMetricsQuery,
|
||||
mockFilters,
|
||||
source === 'logs' ? 'view_logs' : 'view_traces',
|
||||
'metrics_query',
|
||||
);
|
||||
|
||||
const expr = result?.builder.queryData[0]?.filter?.expression || '';
|
||||
// Should remain unchanged (no metric-to-logs/traces transformations)
|
||||
expect(expr).toContain('operation = "GET"');
|
||||
expect(expr).toContain('span.kind = SPAN_KIND_SERVER');
|
||||
expect(expr).toContain('status.code = STATUS_CODE_OK');
|
||||
|
||||
// And should not contain transformed counterparts
|
||||
expect(expr).not.toContain(`name = 'GET'`);
|
||||
expect(expr).not.toContain(`kind = '2'`);
|
||||
expect(expr).not.toContain(`status_code_string = 'Ok'`);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,6 +5,10 @@ import {
|
||||
OPERATORS,
|
||||
} from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import {
|
||||
METRIC_TO_LOGS_TRACES_MAPPINGS,
|
||||
replaceKeysAndValuesInExpression,
|
||||
} from 'container/QueryTable/Drilldown/metricsCorrelationUtils';
|
||||
import cloneDeep from 'lodash-es/cloneDeep';
|
||||
import {
|
||||
BaseAutocompleteData,
|
||||
@@ -270,125 +274,6 @@ const VIEW_QUERY_MAP: Record<string, IBuilderQuery> = {
|
||||
view_traces: initialQueryBuilderFormValuesMap.traces,
|
||||
};
|
||||
|
||||
/**
|
||||
* TEMP LOGIC - TO BE REMOVED LATER
|
||||
* Transforms metric query filters to logs/traces format
|
||||
* Applies the following transformations:
|
||||
* - Rule 2: operation → name
|
||||
* - Rule 3: span.kind → kind
|
||||
* - Rule 4: status.code → status_code_string with value mapping
|
||||
* - Rule 5: http.status_code type conversion
|
||||
*/
|
||||
const transformMetricsToLogsTraces = (
|
||||
filterExpression: string | undefined,
|
||||
): string | undefined => {
|
||||
if (!filterExpression) return filterExpression;
|
||||
|
||||
// ===========================================
|
||||
// MAPPING OBJECTS - ALL TRANSFORMATIONS DEFINED HERE
|
||||
// ===========================================
|
||||
const METRIC_TO_LOGS_TRACES_MAPPINGS = {
|
||||
// Rule 2: operation → name
|
||||
attributeRenames: {
|
||||
operation: 'name',
|
||||
},
|
||||
|
||||
// Rule 3: span.kind → kind with value mapping
|
||||
spanKindMapping: {
|
||||
attribute: 'span.kind',
|
||||
newAttribute: 'kind',
|
||||
valueMappings: {
|
||||
SPAN_KIND_INTERNAL: '1',
|
||||
SPAN_KIND_SERVER: '2',
|
||||
SPAN_KIND_CLIENT: '3',
|
||||
SPAN_KIND_PRODUCER: '4',
|
||||
SPAN_KIND_CONSUMER: '5',
|
||||
},
|
||||
},
|
||||
|
||||
// Rule 4: status.code → status_code_string with value mapping
|
||||
statusCodeMapping: {
|
||||
attribute: 'status.code',
|
||||
newAttribute: 'status_code_string',
|
||||
valueMappings: {
|
||||
// From metrics format → To logs/traces format
|
||||
STATUS_CODE_UNSET: 'Unset',
|
||||
STATUS_CODE_OK: 'Ok',
|
||||
STATUS_CODE_ERROR: 'Error',
|
||||
},
|
||||
},
|
||||
|
||||
// Rule 5: http.status_code type conversion
|
||||
typeConversions: {
|
||||
'http.status_code': 'number',
|
||||
},
|
||||
};
|
||||
// ===========================================
|
||||
|
||||
let transformedExpression = filterExpression;
|
||||
|
||||
// Apply attribute renames
|
||||
Object.entries(METRIC_TO_LOGS_TRACES_MAPPINGS.attributeRenames).forEach(
|
||||
([oldAttr, newAttr]) => {
|
||||
const regex = new RegExp(`\\b${oldAttr}\\b`, 'g');
|
||||
transformedExpression = transformedExpression.replace(regex, newAttr);
|
||||
},
|
||||
);
|
||||
|
||||
// Apply span.kind → kind transformation
|
||||
const { spanKindMapping } = METRIC_TO_LOGS_TRACES_MAPPINGS;
|
||||
if (spanKindMapping) {
|
||||
// Replace attribute name - use word boundaries to avoid partial matches
|
||||
const attrRegex = new RegExp(
|
||||
`\\b${spanKindMapping.attribute.replace(/\./g, '\\.')}\\b`,
|
||||
'g',
|
||||
);
|
||||
transformedExpression = transformedExpression.replace(
|
||||
attrRegex,
|
||||
spanKindMapping.newAttribute,
|
||||
);
|
||||
|
||||
// Replace values
|
||||
Object.entries(spanKindMapping.valueMappings).forEach(
|
||||
([oldValue, newValue]) => {
|
||||
const valueRegex = new RegExp(`\\b${oldValue}\\b`, 'g');
|
||||
transformedExpression = transformedExpression.replace(valueRegex, newValue);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Apply status.code → status_code_string transformation
|
||||
const { statusCodeMapping } = METRIC_TO_LOGS_TRACES_MAPPINGS;
|
||||
if (statusCodeMapping) {
|
||||
// Replace attribute name - use word boundaries to avoid partial matches
|
||||
// This prevents http.status_code from being transformed
|
||||
const attrRegex = new RegExp(
|
||||
`\\b${statusCodeMapping.attribute.replace(/\./g, '\\.')}\\b`,
|
||||
'g',
|
||||
);
|
||||
transformedExpression = transformedExpression.replace(
|
||||
attrRegex,
|
||||
statusCodeMapping.newAttribute,
|
||||
);
|
||||
|
||||
// Replace values
|
||||
Object.entries(statusCodeMapping.valueMappings).forEach(
|
||||
([oldValue, newValue]) => {
|
||||
const valueRegex = new RegExp(`\\b${oldValue}\\b`, 'g');
|
||||
transformedExpression = transformedExpression.replace(
|
||||
valueRegex,
|
||||
`${newValue}`,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Note: Type conversions (Rule 5) would need more complex parsing
|
||||
// of the filter expression to implement properly
|
||||
|
||||
return transformedExpression;
|
||||
};
|
||||
|
||||
export const getViewQuery = (
|
||||
query: Query,
|
||||
filtersToAdd: FilterData[],
|
||||
@@ -448,9 +333,12 @@ export const getViewQuery = (
|
||||
// TEMP LOGIC - TO BE REMOVED LATER
|
||||
// ===========================================
|
||||
// Apply metric-to-logs/traces transformations
|
||||
if (key === 'view_logs' || key === 'view_traces') {
|
||||
const transformedExpression = transformMetricsToLogsTraces(
|
||||
newFilterExpression?.expression,
|
||||
const isMetricQuery =
|
||||
getQueryData(query, queryName)?.dataSource === 'metrics';
|
||||
if (isMetricQuery) {
|
||||
const transformedExpression = replaceKeysAndValuesInExpression(
|
||||
newFilterExpression?.expression || '',
|
||||
METRIC_TO_LOGS_TRACES_MAPPINGS,
|
||||
);
|
||||
newQuery.builder.queryData[0].filter = {
|
||||
expression: transformedExpression || '',
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
/* eslint-disable no-continue */
|
||||
import { formatValueForExpression } from 'components/QueryBuilderV2/utils';
|
||||
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
|
||||
import { IQueryPair } from 'types/antlrQueryTypes';
|
||||
import { extractQueryPairs } from 'utils/queryContextUtils';
|
||||
import { isQuoted, unquote } from 'utils/stringUtils';
|
||||
import { isFunctionOperator, isNonValueOperator } from 'utils/tokenUtils';
|
||||
|
||||
type KeyValueMapping = {
|
||||
attribute: string;
|
||||
newAttribute: string;
|
||||
valueMappings: Record<string, string>;
|
||||
};
|
||||
|
||||
export const METRIC_TO_LOGS_TRACES_MAPPINGS: KeyValueMapping[] = [
|
||||
{
|
||||
attribute: 'operation',
|
||||
newAttribute: 'name',
|
||||
valueMappings: {},
|
||||
},
|
||||
{
|
||||
attribute: 'span.kind',
|
||||
newAttribute: 'kind',
|
||||
valueMappings: {
|
||||
SPAN_KIND_INTERNAL: '1',
|
||||
SPAN_KIND_SERVER: '2',
|
||||
SPAN_KIND_CLIENT: '3',
|
||||
SPAN_KIND_PRODUCER: '4',
|
||||
SPAN_KIND_CONSUMER: '5',
|
||||
},
|
||||
},
|
||||
{
|
||||
attribute: 'status.code',
|
||||
newAttribute: 'status_code_string',
|
||||
valueMappings: {
|
||||
STATUS_CODE_UNSET: 'Unset',
|
||||
STATUS_CODE_OK: 'Ok',
|
||||
STATUS_CODE_ERROR: 'Error',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// Logic for rewriting key/values in an expression using provided mappings.
|
||||
function modifyKeyVal(pair: IQueryPair, mapping: KeyValueMapping): string {
|
||||
const newKey = mapping.newAttribute;
|
||||
const op = pair.operator;
|
||||
|
||||
const operator = pair.hasNegation
|
||||
? getOperatorValue(`NOT_${pair.operator}`.toUpperCase())
|
||||
: getOperatorValue(pair.operator.toUpperCase());
|
||||
|
||||
// Map a single value token using valueMappings, skipping variables
|
||||
const mapOne = (val: string | undefined): string | undefined => {
|
||||
if (val == null) return val;
|
||||
const t = String(val).trim();
|
||||
// Skip variables for now. We will handle them later.
|
||||
if (t.startsWith('$')) return t;
|
||||
const raw = isQuoted(t) ? unquote(t) : t;
|
||||
return mapping.valueMappings[raw] ?? raw;
|
||||
};
|
||||
|
||||
// Function-style operator: op(newKey, value?)
|
||||
if (isFunctionOperator(op)) {
|
||||
let mapped: string | string[] | undefined;
|
||||
if (pair.isMultiValue && Array.isArray(pair.valueList)) {
|
||||
mapped = pair.valueList.map((v) => mapOne(v) as string);
|
||||
} else if (typeof pair.value !== 'undefined') {
|
||||
mapped = mapOne(pair.value);
|
||||
}
|
||||
const hasValue =
|
||||
typeof mapped !== 'undefined' &&
|
||||
!(Array.isArray(mapped) && mapped.length === 0);
|
||||
if (!hasValue) {
|
||||
return `${op}(${newKey})`;
|
||||
}
|
||||
const formatted = formatValueForExpression(mapped as any, op);
|
||||
return `${op}(${newKey}, ${formatted})`;
|
||||
}
|
||||
|
||||
// Non-value operator: e.g., exists / not exists
|
||||
if (isNonValueOperator(op)) {
|
||||
return `${newKey} ${operator}`;
|
||||
}
|
||||
|
||||
// Standard key-operator-value
|
||||
let mapped: string | string[] | undefined;
|
||||
if (pair.isMultiValue && Array.isArray(pair.valueList)) {
|
||||
mapped = pair.valueList.map((v) => mapOne(v) as string);
|
||||
} else if (typeof pair.value !== 'undefined') {
|
||||
mapped = mapOne(pair.value);
|
||||
}
|
||||
const formatted = formatValueForExpression(mapped as any, op);
|
||||
return `${newKey} ${operator} ${formatted}`;
|
||||
}
|
||||
|
||||
// Replace keys/values in an expression using provided mappings.
|
||||
// wires parsing, ordering, and reconstruction.
|
||||
export function replaceKeysAndValuesInExpression(
|
||||
expression: string,
|
||||
mappingList: KeyValueMapping[],
|
||||
): string {
|
||||
if (!expression || !mappingList || mappingList.length === 0) {
|
||||
return expression;
|
||||
}
|
||||
|
||||
const attributeToMapping = new Map<string, KeyValueMapping>(
|
||||
mappingList.map((m) => [m.attribute.trim().toLowerCase(), m]),
|
||||
);
|
||||
|
||||
const pairs: IQueryPair[] = extractQueryPairs(expression);
|
||||
|
||||
type PairWithBounds = {
|
||||
pair: IQueryPair;
|
||||
start: number;
|
||||
end: number;
|
||||
};
|
||||
|
||||
const withBounds: PairWithBounds[] = [];
|
||||
|
||||
for (let i = 0; i < pairs.length; i += 1) {
|
||||
const pair = pairs[i];
|
||||
// Require complete positions for safe slicing
|
||||
if (!pair?.position) continue;
|
||||
const start =
|
||||
pair.position.keyStart ??
|
||||
pair.position.operatorStart ??
|
||||
pair.position.valueStart;
|
||||
const end =
|
||||
pair.position.valueEnd ?? pair.position.operatorEnd ?? pair.position.keyEnd;
|
||||
|
||||
if (
|
||||
typeof start === 'number' &&
|
||||
typeof end === 'number' &&
|
||||
start >= 0 &&
|
||||
end >= start
|
||||
) {
|
||||
withBounds.push({ pair, start, end });
|
||||
}
|
||||
}
|
||||
|
||||
// Process in source order
|
||||
withBounds.sort((a, b) => a.start - b.start);
|
||||
|
||||
let startIdx = 0;
|
||||
const resultParts: string[] = [];
|
||||
|
||||
for (let i = 0; i < withBounds.length; i += 1) {
|
||||
const item = withBounds[i];
|
||||
const sourceKey = item.pair?.key?.trim().toLowerCase();
|
||||
if (!sourceKey) continue;
|
||||
|
||||
const mapping = attributeToMapping.get(sourceKey);
|
||||
if (!mapping) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Add unchanged prefix up to the start of this pair
|
||||
resultParts.push(expression.slice(startIdx, item.start));
|
||||
|
||||
// Replacement produced by modifyKeyVal
|
||||
const replacement = modifyKeyVal(item.pair, mapping);
|
||||
|
||||
resultParts.push(replacement);
|
||||
|
||||
// Advance cursor past this pair
|
||||
startIdx = item.end + 1;
|
||||
}
|
||||
|
||||
// Append the remainder of the expression
|
||||
resultParts.push(expression.slice(startIdx));
|
||||
|
||||
return resultParts.join('');
|
||||
}
|
||||
Reference in New Issue
Block a user