mirror of
https://github.com/SigNoz/signoz.git
synced 2025-12-29 08:00:59 +00:00
Compare commits
3 Commits
fix/qb-lim
...
issue/8880
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84c6c2eb8c | ||
|
|
f6da9adb86 | ||
|
|
c82f54b548 |
@@ -853,7 +853,7 @@ paths:
|
||||
get:
|
||||
deprecated: false
|
||||
description: This endpoints promotes and indexes paths
|
||||
operationId: PromotePaths
|
||||
operationId: ListPromotedAndIndexedPaths
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
@@ -883,13 +883,11 @@ paths:
|
||||
description: Internal Server Error
|
||||
summary: Promote and index paths
|
||||
tags:
|
||||
- promoted_paths
|
||||
- logs
|
||||
- json_logs
|
||||
post:
|
||||
deprecated: false
|
||||
description: This endpoints promotes and indexes paths
|
||||
operationId: PromotePaths
|
||||
operationId: HandlePromoteAndIndexPaths
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
@@ -915,9 +913,7 @@ paths:
|
||||
description: Internal Server Error
|
||||
summary: Promote and index paths
|
||||
tags:
|
||||
- promoted_paths
|
||||
- logs
|
||||
- json_logs
|
||||
/api/v1/org/preferences:
|
||||
get:
|
||||
deprecated: false
|
||||
|
||||
@@ -50,13 +50,6 @@
|
||||
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,7 +375,6 @@ 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"
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
/* 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,7 +242,6 @@ export function Formula({
|
||||
</div>
|
||||
<InputWithLabel
|
||||
label="Limit"
|
||||
type="number"
|
||||
onChange={(value): void => handleChangeLimit(Number(value))}
|
||||
initialValue={formula?.limit ?? undefined}
|
||||
placeholder="Enter limit"
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
.selectOptionContainer {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
overflow-x: auto;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0.2rem;
|
||||
height: 0.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.option-renderer-tooltip {
|
||||
pointer-events: none;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import './QueryBuilderSearch.styles.scss';
|
||||
import './OptionRenderer.styles.scss';
|
||||
|
||||
import { Tooltip } from 'antd';
|
||||
|
||||
@@ -13,7 +13,11 @@ function OptionRenderer({
|
||||
return (
|
||||
<span className="option">
|
||||
{type ? (
|
||||
<Tooltip title={`${value}`} placement="topLeft">
|
||||
<Tooltip
|
||||
title={`${value}`}
|
||||
placement="topLeft"
|
||||
rootClassName="option-renderer-tooltip"
|
||||
>
|
||||
<div className="selectOptionContainer">
|
||||
<div className="option-value">{value}</div>
|
||||
<div className="option-meta-data-container">
|
||||
@@ -29,7 +33,11 @@ function OptionRenderer({
|
||||
</div>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Tooltip title={label} placement="topLeft">
|
||||
<Tooltip
|
||||
title={label}
|
||||
placement="topLeft"
|
||||
rootClassName="option-renderer-tooltip"
|
||||
>
|
||||
<span>{label}</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
@@ -5,19 +5,6 @@
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.selectOptionContainer {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
overflow-x: auto;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0.2rem;
|
||||
height: 0.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.logs-popup {
|
||||
&.hide-scroll {
|
||||
.rc-virtual-list-holder {
|
||||
|
||||
@@ -8,7 +8,6 @@ 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
|
||||
@@ -162,16 +161,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');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -193,16 +192,40 @@ 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');
|
||||
}
|
||||
});
|
||||
|
||||
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');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -235,10 +258,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');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -276,12 +299,13 @@ describe('drilldownUtils', () => {
|
||||
});
|
||||
|
||||
it('should handle all status code value mappings correctly', () => {
|
||||
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;
|
||||
const statusCodeTests = [
|
||||
{ input: 'STATUS_CODE_UNSET', expected: 'Unset' },
|
||||
{ input: 'STATUS_CODE_OK', expected: 'Ok' },
|
||||
{ input: 'STATUS_CODE_ERROR', expected: 'Error' },
|
||||
];
|
||||
|
||||
Object.entries(statusMap).forEach(([input, expected]) => {
|
||||
statusCodeTests.forEach(({ input, expected }) => {
|
||||
const testQuery: Query = {
|
||||
...mockMetricsQuery,
|
||||
builder: {
|
||||
@@ -305,18 +329,19 @@ 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 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;
|
||||
const statusCodeTests = [
|
||||
{ input: '"STATUS_CODE_UNSET"', expected: '"Unset"' },
|
||||
{ input: '"STATUS_CODE_OK"', expected: '"Ok"' },
|
||||
{ input: '"STATUS_CODE_ERROR"', expected: '"Error"' },
|
||||
];
|
||||
|
||||
Object.entries(statusMap).forEach(([input, expected]) => {
|
||||
statusCodeTests.forEach(({ input, expected }) => {
|
||||
const testQuery: Query = {
|
||||
...mockMetricsQuery,
|
||||
builder: {
|
||||
@@ -325,7 +350,7 @@ describe('drilldownUtils', () => {
|
||||
{
|
||||
...mockMetricsQuery.builder.queryData[0],
|
||||
filter: {
|
||||
expression: `status.code = "${input}"`,
|
||||
expression: `status.code = ${input}`,
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -341,8 +366,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}`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -373,8 +398,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"');
|
||||
@@ -383,12 +408,15 @@ describe('drilldownUtils', () => {
|
||||
});
|
||||
|
||||
it('should handle all span.kind value mappings correctly', () => {
|
||||
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;
|
||||
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' },
|
||||
];
|
||||
|
||||
Object.entries(spanKindMap).forEach(([input, expected]) => {
|
||||
spanKindTests.forEach(({ input, expected }) => {
|
||||
const testQuery: Query = {
|
||||
...mockMetricsQuery,
|
||||
builder: {
|
||||
@@ -412,48 +440,9 @@ 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,10 +5,6 @@ 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,
|
||||
@@ -274,6 +270,125 @@ 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[],
|
||||
@@ -333,12 +448,9 @@ export const getViewQuery = (
|
||||
// TEMP LOGIC - TO BE REMOVED LATER
|
||||
// ===========================================
|
||||
// Apply metric-to-logs/traces transformations
|
||||
const isMetricQuery =
|
||||
getQueryData(query, queryName)?.dataSource === 'metrics';
|
||||
if (isMetricQuery) {
|
||||
const transformedExpression = replaceKeysAndValuesInExpression(
|
||||
newFilterExpression?.expression || '',
|
||||
METRIC_TO_LOGS_TRACES_MAPPINGS,
|
||||
if (key === 'view_logs' || key === 'view_traces') {
|
||||
const transformedExpression = transformMetricsToLogsTraces(
|
||||
newFilterExpression?.expression,
|
||||
);
|
||||
newQuery.builder.queryData[0].filter = {
|
||||
expression: transformedExpression || '',
|
||||
|
||||
@@ -1,174 +0,0 @@
|
||||
/* 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('');
|
||||
}
|
||||
@@ -15,7 +15,7 @@ function NoData(): JSX.Element {
|
||||
<Typography.Text className="not-found-text-1">
|
||||
Uh-oh! We cannot show the selected trace.
|
||||
<span className="not-found-text-2">
|
||||
This can happen in either of the two scenraios -
|
||||
This can happen in either of the two scenarios -
|
||||
</span>
|
||||
</Typography.Text>
|
||||
</section>
|
||||
|
||||
@@ -10,8 +10,8 @@ import (
|
||||
|
||||
func (provider *provider) addPromoteRoutes(router *mux.Router) error {
|
||||
if err := router.Handle("/api/v1/logs/promote_paths", handler.New(provider.authZ.EditAccess(provider.promoteHandler.HandlePromoteAndIndexPaths), handler.OpenAPIDef{
|
||||
ID: "PromotePaths",
|
||||
Tags: []string{"promoted_paths", "logs", "json_logs"},
|
||||
ID: "HandlePromoteAndIndexPaths",
|
||||
Tags: []string{"logs"},
|
||||
Summary: "Promote and index paths",
|
||||
Description: "This endpoints promotes and indexes paths",
|
||||
Request: new([]*promotetypes.PromotePath),
|
||||
@@ -25,8 +25,8 @@ func (provider *provider) addPromoteRoutes(router *mux.Router) error {
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/logs/promote_paths", handler.New(provider.authZ.ViewAccess(provider.promoteHandler.ListPromotedAndIndexedPaths), handler.OpenAPIDef{
|
||||
ID: "PromotePaths",
|
||||
Tags: []string{"promoted_paths", "logs", "json_logs"},
|
||||
ID: "ListPromotedAndIndexedPaths",
|
||||
Tags: []string{"logs"},
|
||||
Summary: "Promote and index paths",
|
||||
Description: "This endpoints promotes and indexes paths",
|
||||
Request: nil,
|
||||
|
||||
149
pkg/telemetrylogs/json_access_pb.go
Normal file
149
pkg/telemetrylogs/json_access_pb.go
Normal file
@@ -0,0 +1,149 @@
|
||||
package telemetrylogs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
)
|
||||
|
||||
var (
|
||||
CodePlanIndexOutOfBounds = errors.MustNewCode("plan_index_out_of_bounds")
|
||||
)
|
||||
|
||||
type JSONAccessPlanBuilder struct {
|
||||
key *telemetrytypes.TelemetryFieldKey
|
||||
value any
|
||||
op qbtypes.FilterOperator
|
||||
parts []string
|
||||
getTypes func(ctx context.Context, path string) ([]telemetrytypes.JSONDataType, error)
|
||||
isPromoted bool
|
||||
}
|
||||
|
||||
// buildPlan recursively builds the path plan tree
|
||||
func (pb *JSONAccessPlanBuilder) buildPlan(ctx context.Context, index int, parent *telemetrytypes.JSONAccessNode, isDynArrChild bool) (*telemetrytypes.JSONAccessNode, error) {
|
||||
if index >= len(pb.parts) {
|
||||
return nil, errors.NewInvalidInputf(CodePlanIndexOutOfBounds, "index is out of bounds")
|
||||
}
|
||||
|
||||
part := pb.parts[index]
|
||||
pathSoFar := strings.Join(pb.parts[:index+1], ArraySep)
|
||||
isTerminal := index == len(pb.parts)-1
|
||||
|
||||
// Calculate progression parameters based on parent's values
|
||||
var maxTypes, maxPaths int
|
||||
if isDynArrChild {
|
||||
// Child of Dynamic array - reset progression to base values (16, 256)
|
||||
// This happens when we switch from Array(Dynamic) to Array(JSON)
|
||||
maxTypes = 16
|
||||
maxPaths = 256
|
||||
} else if parent != nil {
|
||||
// Child of JSON array - use parent's progression divided by 2 and 4
|
||||
maxTypes = parent.MaxDynamicTypes / 2
|
||||
maxPaths = parent.MaxDynamicPaths / 4
|
||||
if maxTypes < 0 {
|
||||
maxTypes = 0
|
||||
}
|
||||
if maxPaths < 0 {
|
||||
maxPaths = 0
|
||||
}
|
||||
}
|
||||
|
||||
types, err := pb.getTypes(ctx, pathSoFar)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create node for this path segment
|
||||
node := &telemetrytypes.JSONAccessNode{
|
||||
Name: part,
|
||||
IsTerminal: isTerminal,
|
||||
AvailableTypes: types,
|
||||
Branches: make(map[telemetrytypes.JSONAccessBranchType]*telemetrytypes.JSONAccessNode),
|
||||
Parent: parent,
|
||||
MaxDynamicTypes: maxTypes,
|
||||
MaxDynamicPaths: maxPaths,
|
||||
}
|
||||
|
||||
hasJSON := slices.Contains(node.AvailableTypes, telemetrytypes.ArrayJSON)
|
||||
hasDynamic := slices.Contains(node.AvailableTypes, telemetrytypes.ArrayDynamic)
|
||||
|
||||
// Configure terminal if this is the last part
|
||||
if isTerminal {
|
||||
valueType, _ := inferDataType(pb.value, pb.op, pb.key)
|
||||
node.TerminalConfig = &telemetrytypes.TerminalConfig{
|
||||
Key: pb.key,
|
||||
ElemType: *pb.key.JSONDataType,
|
||||
ValueType: telemetrytypes.MappingFieldDataTypeToJSONDataType[valueType],
|
||||
}
|
||||
} else {
|
||||
if hasJSON {
|
||||
node.Branches[telemetrytypes.BranchJSON], err = pb.buildPlan(ctx, index+1, node, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if hasDynamic {
|
||||
node.Branches[telemetrytypes.BranchDynamic], err = pb.buildPlan(ctx, index+1, node, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return node, nil
|
||||
}
|
||||
|
||||
// PlanJSON builds a tree structure representing the complete JSON path traversal
|
||||
// that precomputes all possible branches and their types
|
||||
func PlanJSON(ctx context.Context, key *telemetrytypes.TelemetryFieldKey, op qbtypes.FilterOperator,
|
||||
value any,
|
||||
getTypes func(ctx context.Context, path string) ([]telemetrytypes.JSONDataType, error),
|
||||
) (telemetrytypes.JSONAccessPlan, error) {
|
||||
// if path is empty, return nil
|
||||
if key.Name == "" {
|
||||
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "path is empty")
|
||||
}
|
||||
|
||||
// TODO: PlanJSON requires the Start and End of the Query to select correct column between promoted and body_json using
|
||||
// creation time in distributed_promoted_paths
|
||||
path := strings.ReplaceAll(key.Name, ArrayAnyIndex, ArraySep)
|
||||
parts := strings.Split(path, ArraySep)
|
||||
|
||||
pb := &JSONAccessPlanBuilder{
|
||||
key: key,
|
||||
op: op,
|
||||
value: value,
|
||||
parts: parts,
|
||||
getTypes: getTypes,
|
||||
isPromoted: key.Materialized,
|
||||
}
|
||||
plans := telemetrytypes.JSONAccessPlan{}
|
||||
|
||||
node, err := pb.buildPlan(ctx, 0,
|
||||
telemetrytypes.NewRootJSONAccessNode(LogsV2BodyJSONColumn,
|
||||
32, 0),
|
||||
false,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
plans = append(plans, node)
|
||||
|
||||
if pb.isPromoted {
|
||||
node, err := pb.buildPlan(ctx, 0,
|
||||
telemetrytypes.NewRootJSONAccessNode(LogsV2BodyPromotedColumn,
|
||||
32, 1024),
|
||||
true,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
plans = append(plans, node)
|
||||
}
|
||||
|
||||
return plans, nil
|
||||
}
|
||||
880
pkg/telemetrylogs/json_access_pb_test.go
Normal file
880
pkg/telemetrylogs/json_access_pb_test.go
Normal file
@@ -0,0 +1,880 @@
|
||||
package telemetrylogs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// ============================================================================
|
||||
// Helper Functions for Test Data Creation
|
||||
// ============================================================================
|
||||
|
||||
// makeKey creates a TelemetryFieldKey for testing
|
||||
func makeKey(name string, dataType telemetrytypes.JSONDataType, materialized bool) *telemetrytypes.TelemetryFieldKey {
|
||||
return &telemetrytypes.TelemetryFieldKey{
|
||||
Name: name,
|
||||
JSONDataType: &dataType,
|
||||
Materialized: materialized,
|
||||
}
|
||||
}
|
||||
|
||||
// makeGetTypes creates a getTypes function from a map of path -> types
|
||||
func makeGetTypes(typesMap map[string][]telemetrytypes.JSONDataType) func(ctx context.Context, path string) ([]telemetrytypes.JSONDataType, error) {
|
||||
return func(_ context.Context, path string) ([]telemetrytypes.JSONDataType, error) {
|
||||
return typesMap[path], nil
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Helper Functions for Node Validation
|
||||
// ============================================================================
|
||||
|
||||
// jsonAccessTestNode is a test-only, YAML-friendly view of JSONAccessNode.
|
||||
// It intentionally omits Parent to avoid cycles and only keeps the fields
|
||||
// that are useful for understanding / asserting the plan structure.
|
||||
type jsonAccessTestNode struct {
|
||||
Name string `yaml:"name"`
|
||||
Column string `yaml:"column,omitempty"`
|
||||
IsTerminal bool `yaml:"isTerminal,omitempty"`
|
||||
MaxDynamicTypes int `yaml:"maxDynamicTypes,omitempty"`
|
||||
MaxDynamicPaths int `yaml:"maxDynamicPaths,omitempty"`
|
||||
ElemType string `yaml:"elemType,omitempty"`
|
||||
ValueType string `yaml:"valueType,omitempty"`
|
||||
AvailableTypes []string `yaml:"availableTypes,omitempty"`
|
||||
Branches map[string]*jsonAccessTestNode `yaml:"branches,omitempty"`
|
||||
}
|
||||
|
||||
// toTestNode converts a JSONAccessNode tree into jsonAccessTestNode so that
|
||||
// it can be serialized to YAML for easy visual comparison in tests.
|
||||
func toTestNode(n *telemetrytypes.JSONAccessNode) *jsonAccessTestNode {
|
||||
if n == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
out := &jsonAccessTestNode{
|
||||
Name: n.Name,
|
||||
IsTerminal: n.IsTerminal,
|
||||
MaxDynamicTypes: n.MaxDynamicTypes,
|
||||
MaxDynamicPaths: n.MaxDynamicPaths,
|
||||
}
|
||||
|
||||
// Column information for top-level plan nodes: their parent is the root,
|
||||
// whose parent is nil.
|
||||
if n.Parent != nil && n.Parent.Parent == nil {
|
||||
out.Column = n.Parent.Name
|
||||
}
|
||||
|
||||
// AvailableTypes as strings (using StringValue for stable representation)
|
||||
if len(n.AvailableTypes) > 0 {
|
||||
out.AvailableTypes = make([]string, 0, len(n.AvailableTypes))
|
||||
for _, t := range n.AvailableTypes {
|
||||
out.AvailableTypes = append(out.AvailableTypes, t.StringValue())
|
||||
}
|
||||
}
|
||||
|
||||
// Terminal config
|
||||
if n.TerminalConfig != nil {
|
||||
out.ElemType = n.TerminalConfig.ElemType.StringValue()
|
||||
out.ValueType = n.TerminalConfig.ValueType.StringValue()
|
||||
}
|
||||
|
||||
// Branches
|
||||
if len(n.Branches) > 0 {
|
||||
out.Branches = make(map[string]*jsonAccessTestNode, len(n.Branches))
|
||||
for bt, child := range n.Branches {
|
||||
out.Branches[bt.StringValue()] = toTestNode(child)
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// plansToYAML converts a slice of JSONAccessNode plans to a YAML string that
|
||||
// can be compared against a per-test expectedTree.
|
||||
func plansToYAML(t *testing.T, plans []*telemetrytypes.JSONAccessNode) string {
|
||||
t.Helper()
|
||||
|
||||
testNodes := make([]*jsonAccessTestNode, 0, len(plans))
|
||||
for _, p := range plans {
|
||||
testNodes = append(testNodes, toTestNode(p))
|
||||
}
|
||||
|
||||
got, err := yaml.Marshal(testNodes)
|
||||
require.NoError(t, err)
|
||||
return string(got)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Test Cases for Node Methods
|
||||
// ============================================================================
|
||||
|
||||
func TestNode_Alias(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
node *telemetrytypes.JSONAccessNode
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "Root node returns name as-is",
|
||||
node: telemetrytypes.NewRootJSONAccessNode(LogsV2BodyJSONColumn, 32, 0),
|
||||
expected: LogsV2BodyJSONColumn,
|
||||
},
|
||||
{
|
||||
name: "Node without parent returns backticked name",
|
||||
node: &telemetrytypes.JSONAccessNode{
|
||||
Name: "user",
|
||||
Parent: nil,
|
||||
},
|
||||
expected: "`user`",
|
||||
},
|
||||
{
|
||||
name: "Node with root parent uses dot separator",
|
||||
node: &telemetrytypes.JSONAccessNode{
|
||||
Name: "age",
|
||||
Parent: telemetrytypes.NewRootJSONAccessNode(LogsV2BodyJSONColumn, 32, 0),
|
||||
},
|
||||
expected: "`" + LogsV2BodyJSONColumn + ".age`",
|
||||
},
|
||||
{
|
||||
name: "Node with non-root parent uses array separator",
|
||||
node: &telemetrytypes.JSONAccessNode{
|
||||
Name: "name",
|
||||
Parent: &telemetrytypes.JSONAccessNode{
|
||||
Name: "education",
|
||||
Parent: telemetrytypes.NewRootJSONAccessNode(LogsV2BodyJSONColumn, 32, 0),
|
||||
},
|
||||
},
|
||||
expected: "`" + LogsV2BodyJSONColumn + ".education[].name`",
|
||||
},
|
||||
{
|
||||
name: "Nested array path with multiple levels",
|
||||
node: &telemetrytypes.JSONAccessNode{
|
||||
Name: "type",
|
||||
Parent: &telemetrytypes.JSONAccessNode{
|
||||
Name: "awards",
|
||||
Parent: &telemetrytypes.JSONAccessNode{
|
||||
Name: "education",
|
||||
Parent: telemetrytypes.NewRootJSONAccessNode(LogsV2BodyJSONColumn, 32, 0),
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: "`" + LogsV2BodyJSONColumn + ".education[].awards[].type`",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := tt.node.Alias()
|
||||
require.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNode_FieldPath(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
node *telemetrytypes.JSONAccessNode
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "Simple field path from root",
|
||||
node: &telemetrytypes.JSONAccessNode{
|
||||
Name: "user",
|
||||
Parent: telemetrytypes.NewRootJSONAccessNode(LogsV2BodyJSONColumn, 32, 0),
|
||||
},
|
||||
// FieldPath() always wraps the field name in backticks
|
||||
expected: LogsV2BodyJSONColumn + ".`user`",
|
||||
},
|
||||
{
|
||||
name: "Field path with backtick-required key",
|
||||
node: &telemetrytypes.JSONAccessNode{
|
||||
Name: "user-name", // requires backtick
|
||||
Parent: telemetrytypes.NewRootJSONAccessNode(LogsV2BodyJSONColumn, 32, 0),
|
||||
},
|
||||
expected: LogsV2BodyJSONColumn + ".`user-name`",
|
||||
},
|
||||
{
|
||||
name: "Nested field path",
|
||||
node: &telemetrytypes.JSONAccessNode{
|
||||
Name: "age",
|
||||
Parent: &telemetrytypes.JSONAccessNode{
|
||||
Name: "user",
|
||||
Parent: telemetrytypes.NewRootJSONAccessNode(LogsV2BodyJSONColumn, 32, 0),
|
||||
},
|
||||
},
|
||||
// FieldPath() always wraps the field name in backticks
|
||||
expected: "`" + LogsV2BodyJSONColumn + ".user`.`age`",
|
||||
},
|
||||
{
|
||||
name: "Array element field path",
|
||||
node: &telemetrytypes.JSONAccessNode{
|
||||
Name: "name",
|
||||
Parent: &telemetrytypes.JSONAccessNode{
|
||||
Name: "education",
|
||||
Parent: telemetrytypes.NewRootJSONAccessNode(LogsV2BodyJSONColumn, 32, 0),
|
||||
},
|
||||
},
|
||||
// FieldPath() always wraps the field name in backticks
|
||||
expected: "`" + LogsV2BodyJSONColumn + ".education`.`name`",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := tt.node.FieldPath()
|
||||
require.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Test Cases for PlanJSON
|
||||
// ============================================================================
|
||||
|
||||
func TestPlanJSON_BasicStructure(t *testing.T) {
|
||||
_, getTypes := testTypeSet()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
key *telemetrytypes.TelemetryFieldKey
|
||||
expectErr bool
|
||||
expectedYAML string
|
||||
}{
|
||||
{
|
||||
name: "Simple path not promoted",
|
||||
key: makeKey("user.name", telemetrytypes.String, false),
|
||||
expectedYAML: `
|
||||
- name: user.name
|
||||
column: body_json
|
||||
availableTypes:
|
||||
- String
|
||||
maxDynamicTypes: 16
|
||||
isTerminal: true
|
||||
elemType: String
|
||||
valueType: String
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "Simple path promoted",
|
||||
key: makeKey("user.name", telemetrytypes.String, true),
|
||||
expectedYAML: `
|
||||
- name: user.name
|
||||
column: body_json
|
||||
availableTypes:
|
||||
- String
|
||||
maxDynamicTypes: 16
|
||||
isTerminal: true
|
||||
elemType: String
|
||||
valueType: String
|
||||
- name: user.name
|
||||
column: body_json_promoted
|
||||
availableTypes:
|
||||
- String
|
||||
maxDynamicTypes: 16
|
||||
maxDynamicPaths: 256
|
||||
isTerminal: true
|
||||
elemType: String
|
||||
valueType: String
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "Empty path returns error",
|
||||
key: makeKey("", telemetrytypes.String, false),
|
||||
expectErr: true,
|
||||
expectedYAML: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
plans, err := PlanJSON(context.Background(), tt.key, qbtypes.FilterOperatorEqual, "John", getTypes)
|
||||
if tt.expectErr {
|
||||
require.Error(t, err)
|
||||
require.Nil(t, plans)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
got := plansToYAML(t, plans)
|
||||
require.YAMLEq(t, tt.expectedYAML, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlanJSON_ArrayPaths(t *testing.T) {
|
||||
_, getTypes := testTypeSet()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
expectedYAML string
|
||||
}{
|
||||
{
|
||||
name: "Single array level - JSON branch only",
|
||||
path: "education[].name",
|
||||
expectedYAML: `
|
||||
- name: education
|
||||
column: body_json
|
||||
availableTypes:
|
||||
- Array(JSON)
|
||||
maxDynamicTypes: 16
|
||||
branches:
|
||||
json:
|
||||
name: name
|
||||
availableTypes:
|
||||
- String
|
||||
maxDynamicTypes: 8
|
||||
isTerminal: true
|
||||
elemType: String
|
||||
valueType: String
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "Single array level - both JSON and Dynamic branches",
|
||||
path: "education[].awards[].type",
|
||||
expectedYAML: `
|
||||
- name: education
|
||||
column: body_json
|
||||
availableTypes:
|
||||
- Array(JSON)
|
||||
maxDynamicTypes: 16
|
||||
branches:
|
||||
json:
|
||||
name: awards
|
||||
availableTypes:
|
||||
- Array(Dynamic)
|
||||
- Array(JSON)
|
||||
maxDynamicTypes: 8
|
||||
branches:
|
||||
json:
|
||||
name: type
|
||||
availableTypes:
|
||||
- String
|
||||
maxDynamicTypes: 4
|
||||
isTerminal: true
|
||||
elemType: String
|
||||
valueType: String
|
||||
dynamic:
|
||||
name: type
|
||||
availableTypes:
|
||||
- String
|
||||
maxDynamicTypes: 16
|
||||
maxDynamicPaths: 256
|
||||
isTerminal: true
|
||||
elemType: String
|
||||
valueType: String
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "Deeply nested array path",
|
||||
path: "interests[].entities[].reviews[].entries[].metadata[].positions[].name",
|
||||
expectedYAML: `
|
||||
- name: interests
|
||||
column: body_json
|
||||
availableTypes:
|
||||
- Array(JSON)
|
||||
maxDynamicTypes: 16
|
||||
branches:
|
||||
json:
|
||||
name: entities
|
||||
availableTypes:
|
||||
- Array(JSON)
|
||||
maxDynamicTypes: 8
|
||||
branches:
|
||||
json:
|
||||
name: reviews
|
||||
availableTypes:
|
||||
- Array(JSON)
|
||||
maxDynamicTypes: 4
|
||||
branches:
|
||||
json:
|
||||
name: entries
|
||||
availableTypes:
|
||||
- Array(JSON)
|
||||
maxDynamicTypes: 2
|
||||
branches:
|
||||
json:
|
||||
name: metadata
|
||||
availableTypes:
|
||||
- Array(JSON)
|
||||
maxDynamicTypes: 1
|
||||
branches:
|
||||
json:
|
||||
name: positions
|
||||
availableTypes:
|
||||
- Array(JSON)
|
||||
branches:
|
||||
json:
|
||||
name: name
|
||||
availableTypes:
|
||||
- String
|
||||
isTerminal: true
|
||||
elemType: String
|
||||
valueType: String
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "ArrayAnyIndex replacement [*] to []",
|
||||
path: "education[*].name",
|
||||
expectedYAML: `
|
||||
- name: education
|
||||
column: body_json
|
||||
availableTypes:
|
||||
- Array(JSON)
|
||||
maxDynamicTypes: 16
|
||||
branches:
|
||||
json:
|
||||
name: name
|
||||
availableTypes:
|
||||
- String
|
||||
maxDynamicTypes: 8
|
||||
isTerminal: true
|
||||
elemType: String
|
||||
valueType: String
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
key := makeKey(tt.path, telemetrytypes.String, false)
|
||||
plans, err := PlanJSON(context.Background(), key, qbtypes.FilterOperatorEqual, "John", getTypes)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, plans)
|
||||
require.Len(t, plans, 1)
|
||||
got := plansToYAML(t, plans)
|
||||
require.YAMLEq(t, tt.expectedYAML, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlanJSON_PromotedVsNonPromoted(t *testing.T) {
|
||||
_, getTypes := testTypeSet()
|
||||
path := "education[].awards[].type"
|
||||
value := "sports"
|
||||
|
||||
t.Run("Non-promoted plan", func(t *testing.T) {
|
||||
key := makeKey(path, telemetrytypes.String, false)
|
||||
plans, err := PlanJSON(context.Background(), key, qbtypes.FilterOperatorEqual, value, getTypes)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, plans, 1)
|
||||
|
||||
expectedYAML := `
|
||||
- name: education
|
||||
column: body_json
|
||||
availableTypes:
|
||||
- Array(JSON)
|
||||
maxDynamicTypes: 16
|
||||
branches:
|
||||
json:
|
||||
name: awards
|
||||
availableTypes:
|
||||
- Array(Dynamic)
|
||||
- Array(JSON)
|
||||
maxDynamicTypes: 8
|
||||
branches:
|
||||
json:
|
||||
name: type
|
||||
availableTypes:
|
||||
- String
|
||||
maxDynamicTypes: 4
|
||||
isTerminal: true
|
||||
elemType: String
|
||||
valueType: String
|
||||
dynamic:
|
||||
name: type
|
||||
availableTypes:
|
||||
- String
|
||||
maxDynamicTypes: 16
|
||||
maxDynamicPaths: 256
|
||||
isTerminal: true
|
||||
elemType: String
|
||||
valueType: String
|
||||
`
|
||||
got := plansToYAML(t, plans)
|
||||
require.YAMLEq(t, expectedYAML, got)
|
||||
})
|
||||
|
||||
t.Run("Promoted plan", func(t *testing.T) {
|
||||
key := makeKey(path, telemetrytypes.String, true)
|
||||
plans, err := PlanJSON(context.Background(), key, qbtypes.FilterOperatorEqual, value, getTypes)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, plans, 2)
|
||||
|
||||
expectedYAML := `
|
||||
- name: education
|
||||
column: body_json
|
||||
availableTypes:
|
||||
- Array(JSON)
|
||||
maxDynamicTypes: 16
|
||||
branches:
|
||||
json:
|
||||
name: awards
|
||||
availableTypes:
|
||||
- Array(Dynamic)
|
||||
- Array(JSON)
|
||||
maxDynamicTypes: 8
|
||||
branches:
|
||||
json:
|
||||
name: type
|
||||
availableTypes:
|
||||
- String
|
||||
maxDynamicTypes: 4
|
||||
isTerminal: true
|
||||
elemType: String
|
||||
valueType: String
|
||||
dynamic:
|
||||
name: type
|
||||
availableTypes:
|
||||
- String
|
||||
maxDynamicTypes: 16
|
||||
maxDynamicPaths: 256
|
||||
isTerminal: true
|
||||
elemType: String
|
||||
valueType: String
|
||||
- name: education
|
||||
column: body_json_promoted
|
||||
availableTypes:
|
||||
- Array(JSON)
|
||||
maxDynamicTypes: 16
|
||||
maxDynamicPaths: 256
|
||||
branches:
|
||||
json:
|
||||
name: awards
|
||||
availableTypes:
|
||||
- Array(Dynamic)
|
||||
- Array(JSON)
|
||||
maxDynamicTypes: 8
|
||||
maxDynamicPaths: 64
|
||||
branches:
|
||||
json:
|
||||
name: type
|
||||
availableTypes:
|
||||
- String
|
||||
maxDynamicTypes: 4
|
||||
maxDynamicPaths: 16
|
||||
isTerminal: true
|
||||
elemType: String
|
||||
valueType: String
|
||||
dynamic:
|
||||
name: type
|
||||
availableTypes:
|
||||
- String
|
||||
maxDynamicTypes: 16
|
||||
maxDynamicPaths: 256
|
||||
isTerminal: true
|
||||
elemType: String
|
||||
valueType: String
|
||||
`
|
||||
got := plansToYAML(t, plans)
|
||||
require.YAMLEq(t, expectedYAML, got)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPlanJSON_EdgeCases(t *testing.T) {
|
||||
_, getTypes := testTypeSet()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
value any
|
||||
expectedYAML string
|
||||
}{
|
||||
{
|
||||
name: "Path with no available types",
|
||||
path: "unknown.path",
|
||||
value: "test",
|
||||
expectedYAML: `
|
||||
- name: unknown.path
|
||||
column: body_json
|
||||
maxDynamicTypes: 16
|
||||
isTerminal: true
|
||||
elemType: String
|
||||
valueType: String
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "Very deep nesting - validates progression doesn't go negative",
|
||||
path: "interests[].entities[].reviews[].entries[].metadata[].positions[].name",
|
||||
value: "Engineer",
|
||||
expectedYAML: `
|
||||
- name: interests
|
||||
column: body_json
|
||||
availableTypes:
|
||||
- Array(JSON)
|
||||
maxDynamicTypes: 16
|
||||
branches:
|
||||
json:
|
||||
name: entities
|
||||
availableTypes:
|
||||
- Array(JSON)
|
||||
maxDynamicTypes: 8
|
||||
branches:
|
||||
json:
|
||||
name: reviews
|
||||
availableTypes:
|
||||
- Array(JSON)
|
||||
maxDynamicTypes: 4
|
||||
branches:
|
||||
json:
|
||||
name: entries
|
||||
availableTypes:
|
||||
- Array(JSON)
|
||||
maxDynamicTypes: 2
|
||||
branches:
|
||||
json:
|
||||
name: metadata
|
||||
availableTypes:
|
||||
- Array(JSON)
|
||||
maxDynamicTypes: 1
|
||||
branches:
|
||||
json:
|
||||
name: positions
|
||||
availableTypes:
|
||||
- Array(JSON)
|
||||
branches:
|
||||
json:
|
||||
name: name
|
||||
availableTypes:
|
||||
- String
|
||||
isTerminal: true
|
||||
elemType: String
|
||||
valueType: String
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "Path with mixed scalar and array types",
|
||||
path: "education[].type",
|
||||
value: "high_school",
|
||||
expectedYAML: `
|
||||
- name: education
|
||||
column: body_json
|
||||
availableTypes:
|
||||
- Array(JSON)
|
||||
maxDynamicTypes: 16
|
||||
branches:
|
||||
json:
|
||||
name: type
|
||||
availableTypes:
|
||||
- String
|
||||
- Int64
|
||||
maxDynamicTypes: 8
|
||||
isTerminal: true
|
||||
elemType: String
|
||||
valueType: String
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "Exists with only array types available",
|
||||
path: "education",
|
||||
value: nil,
|
||||
expectedYAML: `
|
||||
- name: education
|
||||
column: body_json
|
||||
availableTypes:
|
||||
- Array(JSON)
|
||||
maxDynamicTypes: 16
|
||||
isTerminal: true
|
||||
elemType: Array(JSON)
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Choose key type based on path; operator does not affect the tree shape asserted here.
|
||||
keyType := telemetrytypes.String
|
||||
switch tt.path {
|
||||
case "education":
|
||||
keyType = telemetrytypes.ArrayJSON
|
||||
case "education[].type":
|
||||
keyType = telemetrytypes.String
|
||||
}
|
||||
key := makeKey(tt.path, keyType, false)
|
||||
plans, err := PlanJSON(context.Background(), key, qbtypes.FilterOperatorEqual, tt.value, getTypes)
|
||||
require.NoError(t, err)
|
||||
got := plansToYAML(t, plans)
|
||||
require.YAMLEq(t, tt.expectedYAML, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlanJSON_TreeStructure(t *testing.T) {
|
||||
_, getTypes := testTypeSet()
|
||||
path := "education[].awards[].participated[].team[].branch"
|
||||
key := makeKey(path, telemetrytypes.String, false)
|
||||
plans, err := PlanJSON(context.Background(), key, qbtypes.FilterOperatorEqual, "John", getTypes)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, plans, 1)
|
||||
|
||||
expectedYAML := `
|
||||
- name: education
|
||||
column: body_json
|
||||
availableTypes:
|
||||
- Array(JSON)
|
||||
maxDynamicTypes: 16
|
||||
branches:
|
||||
json:
|
||||
name: awards
|
||||
availableTypes:
|
||||
- Array(Dynamic)
|
||||
- Array(JSON)
|
||||
maxDynamicTypes: 8
|
||||
branches:
|
||||
json:
|
||||
name: participated
|
||||
availableTypes:
|
||||
- Array(Dynamic)
|
||||
- Array(JSON)
|
||||
maxDynamicTypes: 4
|
||||
branches:
|
||||
json:
|
||||
name: team
|
||||
availableTypes:
|
||||
- Array(JSON)
|
||||
maxDynamicTypes: 2
|
||||
branches:
|
||||
json:
|
||||
name: branch
|
||||
availableTypes:
|
||||
- String
|
||||
maxDynamicTypes: 1
|
||||
isTerminal: true
|
||||
elemType: String
|
||||
valueType: String
|
||||
dynamic:
|
||||
name: team
|
||||
availableTypes:
|
||||
- Array(JSON)
|
||||
maxDynamicTypes: 16
|
||||
maxDynamicPaths: 256
|
||||
branches:
|
||||
json:
|
||||
name: branch
|
||||
availableTypes:
|
||||
- String
|
||||
maxDynamicTypes: 8
|
||||
maxDynamicPaths: 64
|
||||
isTerminal: true
|
||||
elemType: String
|
||||
valueType: String
|
||||
dynamic:
|
||||
name: participated
|
||||
availableTypes:
|
||||
- Array(Dynamic)
|
||||
- Array(JSON)
|
||||
maxDynamicTypes: 16
|
||||
maxDynamicPaths: 256
|
||||
branches:
|
||||
json:
|
||||
name: team
|
||||
availableTypes:
|
||||
- Array(JSON)
|
||||
maxDynamicTypes: 8
|
||||
maxDynamicPaths: 64
|
||||
branches:
|
||||
json:
|
||||
name: branch
|
||||
availableTypes:
|
||||
- String
|
||||
maxDynamicTypes: 4
|
||||
maxDynamicPaths: 16
|
||||
isTerminal: true
|
||||
elemType: String
|
||||
valueType: String
|
||||
dynamic:
|
||||
name: team
|
||||
availableTypes:
|
||||
- Array(JSON)
|
||||
maxDynamicTypes: 16
|
||||
maxDynamicPaths: 256
|
||||
branches:
|
||||
json:
|
||||
name: branch
|
||||
availableTypes:
|
||||
- String
|
||||
maxDynamicTypes: 8
|
||||
maxDynamicPaths: 64
|
||||
isTerminal: true
|
||||
elemType: String
|
||||
valueType: String
|
||||
`
|
||||
|
||||
got := plansToYAML(t, plans)
|
||||
require.YAMLEq(t, expectedYAML, got)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Test Data Setup
|
||||
// ============================================================================
|
||||
|
||||
// testTypeSet returns a map of path->types and a getTypes function for testing
|
||||
// This represents the type information available in the test JSON structure
|
||||
//
|
||||
// TODO(Piyush): Remove this unparam nolint
|
||||
// nolint:unparam
|
||||
func testTypeSet() (map[string][]telemetrytypes.JSONDataType, func(ctx context.Context, path string) ([]telemetrytypes.JSONDataType, error)) {
|
||||
types := map[string][]telemetrytypes.JSONDataType{
|
||||
"user.name": {telemetrytypes.String},
|
||||
"user.age": {telemetrytypes.Int64, telemetrytypes.String},
|
||||
"user.height": {telemetrytypes.Float64},
|
||||
"education": {telemetrytypes.ArrayJSON},
|
||||
"education[].name": {telemetrytypes.String},
|
||||
"education[].type": {telemetrytypes.String, telemetrytypes.Int64},
|
||||
"education[].internal_type": {telemetrytypes.String},
|
||||
"education[].metadata.location": {telemetrytypes.String},
|
||||
"education[].parameters": {telemetrytypes.ArrayFloat64, telemetrytypes.ArrayDynamic},
|
||||
"education[].duration": {telemetrytypes.String},
|
||||
"education[].mode": {telemetrytypes.String},
|
||||
"education[].year": {telemetrytypes.Int64},
|
||||
"education[].field": {telemetrytypes.String},
|
||||
"education[].awards": {telemetrytypes.ArrayDynamic, telemetrytypes.ArrayJSON},
|
||||
"education[].awards[].name": {telemetrytypes.String},
|
||||
"education[].awards[].rank": {telemetrytypes.Int64},
|
||||
"education[].awards[].medal": {telemetrytypes.String},
|
||||
"education[].awards[].type": {telemetrytypes.String},
|
||||
"education[].awards[].semester": {telemetrytypes.Int64},
|
||||
"education[].awards[].participated": {telemetrytypes.ArrayDynamic, telemetrytypes.ArrayJSON},
|
||||
"education[].awards[].participated[].type": {telemetrytypes.String},
|
||||
"education[].awards[].participated[].field": {telemetrytypes.String},
|
||||
"education[].awards[].participated[].project_type": {telemetrytypes.String},
|
||||
"education[].awards[].participated[].project_name": {telemetrytypes.String},
|
||||
"education[].awards[].participated[].race_type": {telemetrytypes.String},
|
||||
"education[].awards[].participated[].team_based": {telemetrytypes.Bool},
|
||||
"education[].awards[].participated[].team_name": {telemetrytypes.String},
|
||||
"education[].awards[].participated[].team": {telemetrytypes.ArrayJSON},
|
||||
"education[].awards[].participated[].team[].name": {telemetrytypes.String},
|
||||
"education[].awards[].participated[].team[].branch": {telemetrytypes.String},
|
||||
"education[].awards[].participated[].team[].semester": {telemetrytypes.Int64},
|
||||
"interests": {telemetrytypes.ArrayJSON},
|
||||
"interests[].type": {telemetrytypes.String},
|
||||
"interests[].entities": {telemetrytypes.ArrayJSON},
|
||||
"interests[].entities.application_date": {telemetrytypes.String},
|
||||
"interests[].entities[].reviews": {telemetrytypes.ArrayJSON},
|
||||
"interests[].entities[].reviews[].given_by": {telemetrytypes.String},
|
||||
"interests[].entities[].reviews[].remarks": {telemetrytypes.String},
|
||||
"interests[].entities[].reviews[].weight": {telemetrytypes.Float64},
|
||||
"interests[].entities[].reviews[].passed": {telemetrytypes.Bool},
|
||||
"interests[].entities[].reviews[].type": {telemetrytypes.String},
|
||||
"interests[].entities[].reviews[].analysis_type": {telemetrytypes.Int64},
|
||||
"interests[].entities[].reviews[].entries": {telemetrytypes.ArrayJSON},
|
||||
"interests[].entities[].reviews[].entries[].subject": {telemetrytypes.String},
|
||||
"interests[].entities[].reviews[].entries[].status": {telemetrytypes.String},
|
||||
"interests[].entities[].reviews[].entries[].metadata": {telemetrytypes.ArrayJSON},
|
||||
"interests[].entities[].reviews[].entries[].metadata[].company": {telemetrytypes.String},
|
||||
"interests[].entities[].reviews[].entries[].metadata[].experience": {telemetrytypes.Int64},
|
||||
"interests[].entities[].reviews[].entries[].metadata[].unit": {telemetrytypes.String},
|
||||
"interests[].entities[].reviews[].entries[].metadata[].positions": {telemetrytypes.ArrayJSON},
|
||||
"interests[].entities[].reviews[].entries[].metadata[].positions[].name": {telemetrytypes.String},
|
||||
"interests[].entities[].reviews[].entries[].metadata[].positions[].duration": {telemetrytypes.Int64, telemetrytypes.Float64},
|
||||
"interests[].entities[].reviews[].entries[].metadata[].positions[].unit": {telemetrytypes.String},
|
||||
"interests[].entities[].reviews[].entries[].metadata[].positions[].ratings": {telemetrytypes.ArrayInt64, telemetrytypes.ArrayString},
|
||||
"message": {telemetrytypes.String},
|
||||
}
|
||||
|
||||
return types, makeGetTypes(types)
|
||||
}
|
||||
@@ -245,7 +245,7 @@ func (t *telemetryMetaStore) ListLogsJSONIndexes(ctx context.Context, filters ..
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
indexesMap := make(map[string][]schemamigrator.Index)
|
||||
indexes := make(map[string][]schemamigrator.Index)
|
||||
for rows.Next() {
|
||||
var name string
|
||||
var typeFull string
|
||||
@@ -254,7 +254,7 @@ func (t *telemetryMetaStore) ListLogsJSONIndexes(ctx context.Context, filters ..
|
||||
if err := rows.Scan(&name, &typeFull, &expr, &granularity); err != nil {
|
||||
return nil, errors.WrapInternalf(err, CodeFailLoadLogsJSONIndexes, "failed to scan string indexed column")
|
||||
}
|
||||
indexesMap[name] = append(indexesMap[name], schemamigrator.Index{
|
||||
indexes[name] = append(indexes[name], schemamigrator.Index{
|
||||
Name: name,
|
||||
Type: typeFull,
|
||||
Expression: expr,
|
||||
@@ -262,7 +262,7 @@ func (t *telemetryMetaStore) ListLogsJSONIndexes(ctx context.Context, filters ..
|
||||
})
|
||||
}
|
||||
|
||||
return indexesMap, nil
|
||||
return indexes, nil
|
||||
}
|
||||
|
||||
func (t *telemetryMetaStore) ListPromotedPaths(ctx context.Context, paths ...string) (map[string]struct{}, error) {
|
||||
|
||||
82
pkg/types/telemetrytypes/json_access_plan.go
Normal file
82
pkg/types/telemetrytypes/json_access_plan.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package telemetrytypes
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz-otel-collector/exporter/jsontypeexporter"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
type JSONAccessBranchType struct {
|
||||
valuer.String
|
||||
}
|
||||
|
||||
var (
|
||||
BranchJSON = JSONAccessBranchType{valuer.NewString("json")}
|
||||
BranchDynamic = JSONAccessBranchType{valuer.NewString("dynamic")}
|
||||
)
|
||||
|
||||
type JSONAccessPlan = []*JSONAccessNode
|
||||
|
||||
type TerminalConfig struct {
|
||||
Key *TelemetryFieldKey
|
||||
ElemType JSONDataType
|
||||
ValueType JSONDataType
|
||||
}
|
||||
|
||||
// Node is now a tree structure representing the complete JSON path traversal
|
||||
// that precomputes all possible branches and their types
|
||||
type JSONAccessNode struct {
|
||||
// Node information
|
||||
Name string
|
||||
IsTerminal bool
|
||||
isRoot bool // marked true for only body_json and body_json_promoted
|
||||
|
||||
// Precomputed type information (single source of truth)
|
||||
AvailableTypes []JSONDataType
|
||||
|
||||
// Array type branches (Array(JSON) vs Array(Dynamic))
|
||||
Branches map[JSONAccessBranchType]*JSONAccessNode
|
||||
|
||||
// Terminal configuration
|
||||
TerminalConfig *TerminalConfig
|
||||
|
||||
// Parent reference for traversal
|
||||
Parent *JSONAccessNode
|
||||
|
||||
// JSON progression parameters (precomputed during planning)
|
||||
MaxDynamicTypes int
|
||||
MaxDynamicPaths int
|
||||
}
|
||||
|
||||
func NewRootJSONAccessNode(name string, maxDynamicTypes, maxDynamicPaths int) *JSONAccessNode {
|
||||
return &JSONAccessNode{
|
||||
Name: name,
|
||||
isRoot: true,
|
||||
MaxDynamicTypes: maxDynamicTypes,
|
||||
MaxDynamicPaths: maxDynamicPaths,
|
||||
}
|
||||
}
|
||||
|
||||
func (n *JSONAccessNode) Alias() string {
|
||||
if n.isRoot {
|
||||
return n.Name
|
||||
} else if n.Parent == nil {
|
||||
return fmt.Sprintf("`%s`", n.Name)
|
||||
}
|
||||
|
||||
parentAlias := strings.TrimLeft(n.Parent.Alias(), "`")
|
||||
parentAlias = strings.TrimRight(parentAlias, "`")
|
||||
|
||||
sep := jsontypeexporter.ArraySeparator
|
||||
if n.Parent.isRoot {
|
||||
sep = "."
|
||||
}
|
||||
return fmt.Sprintf("`%s%s%s`", parentAlias, sep, n.Name)
|
||||
}
|
||||
|
||||
func (n *JSONAccessNode) FieldPath() string {
|
||||
key := "`" + n.Name + "`"
|
||||
return n.Parent.Alias() + "." + key
|
||||
}
|
||||
Reference in New Issue
Block a user