mirror of
https://github.com/SigNoz/signoz.git
synced 2026-02-18 23:12:36 +00:00
Compare commits
16 Commits
chore/supp
...
lp-filter-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
029cd38e21 | ||
|
|
421f3bdd9e | ||
|
|
53a0b532bb | ||
|
|
03a13678d0 | ||
|
|
14d4751c96 | ||
|
|
b50d8136a5 | ||
|
|
4103fe4d2f | ||
|
|
ccc102dc7b | ||
|
|
2cd0ab8a28 | ||
|
|
b101ffeae1 | ||
|
|
d7f543ad4a | ||
|
|
f81c64f348 | ||
|
|
40956116dc | ||
|
|
b49a95b7b3 | ||
|
|
ae19bb2be2 | ||
|
|
749f52ff9d |
@@ -2,8 +2,6 @@
|
||||
interface SafeNavigateOptions {
|
||||
replace?: boolean;
|
||||
state?: unknown;
|
||||
newTab?: boolean;
|
||||
event?: MouseEvent | React.MouseEvent;
|
||||
}
|
||||
|
||||
interface SafeNavigateTo {
|
||||
@@ -22,7 +20,9 @@ interface UseSafeNavigateReturn {
|
||||
|
||||
export const useSafeNavigate = (): UseSafeNavigateReturn => ({
|
||||
safeNavigate: jest.fn(
|
||||
(_to: SafeNavigateToType, _options?: SafeNavigateOptions) => {},
|
||||
(to: SafeNavigateToType, options?: SafeNavigateOptions) => {
|
||||
console.log(`Mock safeNavigate called with:`, to, options);
|
||||
},
|
||||
) as jest.MockedFunction<
|
||||
(to: SafeNavigateToType, options?: SafeNavigateOptions) => void
|
||||
>,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useCopyToClipboard, useLocation } from 'react-use';
|
||||
import { Color, Spacing } from '@signozhq/design-tokens';
|
||||
@@ -214,7 +214,7 @@ function LogDetailInner({
|
||||
};
|
||||
|
||||
// Go to logs explorer page with the log data
|
||||
const handleOpenInExplorer = (e?: React.MouseEvent): void => {
|
||||
const handleOpenInExplorer = (): void => {
|
||||
const queryParams = {
|
||||
[QueryParams.activeLogId]: `"${log?.id}"`,
|
||||
[QueryParams.startTime]: minTime?.toString() || '',
|
||||
@@ -227,9 +227,7 @@ function LogDetailInner({
|
||||
),
|
||||
),
|
||||
};
|
||||
safeNavigate(`${ROUTES.LOGS_EXPLORER}?${createQueryParams(queryParams)}`, {
|
||||
event: e,
|
||||
});
|
||||
safeNavigate(`${ROUTES.LOGS_EXPLORER}?${createQueryParams(queryParams)}`);
|
||||
};
|
||||
|
||||
const handleQueryExpressionChange = useCallback(
|
||||
|
||||
@@ -70,8 +70,8 @@ function SelectAlertType({ onSelect }: SelectAlertTypeProps): JSX.Element {
|
||||
</Tag>
|
||||
) : undefined
|
||||
}
|
||||
onClick={(e): void => {
|
||||
onSelect(option.selection, e);
|
||||
onClick={(): void => {
|
||||
onSelect(option.selection);
|
||||
}}
|
||||
data-testid={`alert-type-card-${option.selection}`}
|
||||
>
|
||||
@@ -108,7 +108,7 @@ function SelectAlertType({ onSelect }: SelectAlertTypeProps): JSX.Element {
|
||||
}
|
||||
|
||||
interface SelectAlertTypeProps {
|
||||
onSelect: (type: AlertTypes, event?: React.MouseEvent) => void;
|
||||
onSelect: (typ: AlertTypes) => void;
|
||||
}
|
||||
|
||||
export default SelectAlertType;
|
||||
|
||||
@@ -33,9 +33,9 @@ function Footer(): JSX.Element {
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
|
||||
const handleDiscard = (e: React.MouseEvent): void => {
|
||||
const handleDiscard = (): void => {
|
||||
discardAlertRule();
|
||||
safeNavigate('/alerts', { event: e });
|
||||
safeNavigate('/alerts');
|
||||
};
|
||||
|
||||
const alertValidationMessage = useMemo(
|
||||
|
||||
@@ -161,7 +161,6 @@ describe('Dashboard landing page actions header tests', () => {
|
||||
// Verify navigation was called with correct URL
|
||||
expect(mockSafeNavigate).toHaveBeenCalledWith(
|
||||
'/dashboard?columnKey=updatedAt&order=descend&page=1&search=',
|
||||
expect.objectContaining({ event: expect.any(Object) }),
|
||||
);
|
||||
|
||||
// Ensure the URL contains only essential dashboard list params
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { Button } from 'antd';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
@@ -23,19 +23,16 @@ function DashboardBreadcrumbs(): JSX.Element {
|
||||
|
||||
const { title = '', image = Base64Icons[0] } = selectedData || {};
|
||||
|
||||
const goToListPage = useCallback(
|
||||
(e?: React.MouseEvent) => {
|
||||
const urlParams = new URLSearchParams();
|
||||
urlParams.set('columnKey', listSortOrder.columnKey as string);
|
||||
urlParams.set('order', listSortOrder.order as string);
|
||||
urlParams.set('page', listSortOrder.pagination as string);
|
||||
urlParams.set('search', listSortOrder.search as string);
|
||||
const goToListPage = useCallback(() => {
|
||||
const urlParams = new URLSearchParams();
|
||||
urlParams.set('columnKey', listSortOrder.columnKey as string);
|
||||
urlParams.set('order', listSortOrder.order as string);
|
||||
urlParams.set('page', listSortOrder.pagination as string);
|
||||
urlParams.set('search', listSortOrder.search as string);
|
||||
|
||||
const generatedUrl = `${ROUTES.ALL_DASHBOARD}?${urlParams.toString()}`;
|
||||
safeNavigate(generatedUrl, { event: e });
|
||||
},
|
||||
[listSortOrder, safeNavigate],
|
||||
);
|
||||
const generatedUrl = `${ROUTES.ALL_DASHBOARD}?${urlParams.toString()}`;
|
||||
safeNavigate(generatedUrl);
|
||||
}, [listSortOrder, safeNavigate]);
|
||||
|
||||
return (
|
||||
<div className="dashboard-breadcrumbs">
|
||||
|
||||
@@ -16,7 +16,6 @@ import { isUndefined } from 'lodash-es';
|
||||
import { urlKey } from 'pages/ErrorDetails/utils';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import { PayloadProps as GetByErrorTypeAndServicePayload } from 'types/api/errors/getByErrorTypeAndService';
|
||||
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
|
||||
|
||||
import { keyToExclude } from './config';
|
||||
import { DashedContainer, EditorContainer, EventContainer } from './styles';
|
||||
@@ -112,19 +111,14 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
|
||||
value: errorDetail[key as keyof GetByErrorTypeAndServicePayload],
|
||||
}));
|
||||
|
||||
const onClickTraceHandler = (event: React.MouseEvent): void => {
|
||||
const onClickTraceHandler = (): void => {
|
||||
logEvent('Exception: Navigate to trace detail page', {
|
||||
groupId: errorDetail?.groupID,
|
||||
spanId: errorDetail.spanID,
|
||||
traceId: errorDetail.traceID,
|
||||
exceptionId: errorDetail?.errorId,
|
||||
});
|
||||
const path = `/trace/${errorDetail.traceID}?spanId=${errorDetail.spanID}`;
|
||||
if (isModifierKeyPressed(event)) {
|
||||
openInNewTab(path);
|
||||
} else {
|
||||
history.push(path);
|
||||
}
|
||||
history.push(`/trace/${errorDetail.traceID}?spanId=${errorDetail.spanID}`);
|
||||
};
|
||||
|
||||
const logEventCalledRef = useRef(false);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { useSelector } from 'react-redux';
|
||||
@@ -329,18 +329,13 @@ function FormAlertRules({
|
||||
}
|
||||
}, [alertDef, currentQuery?.queryType, queryOptions]);
|
||||
|
||||
const onCancelHandler = useCallback(
|
||||
(e?: React.MouseEvent) => {
|
||||
urlQuery.delete(QueryParams.compositeQuery);
|
||||
urlQuery.delete(QueryParams.panelTypes);
|
||||
urlQuery.delete(QueryParams.ruleId);
|
||||
urlQuery.delete(QueryParams.relativeTime);
|
||||
safeNavigate(`${ROUTES.LIST_ALL_ALERT}?${urlQuery.toString()}`, {
|
||||
event: e,
|
||||
});
|
||||
},
|
||||
[safeNavigate, urlQuery],
|
||||
);
|
||||
const onCancelHandler = useCallback(() => {
|
||||
urlQuery.delete(QueryParams.compositeQuery);
|
||||
urlQuery.delete(QueryParams.panelTypes);
|
||||
urlQuery.delete(QueryParams.ruleId);
|
||||
urlQuery.delete(QueryParams.relativeTime);
|
||||
safeNavigate(`${ROUTES.LIST_ALL_ALERT}?${urlQuery.toString()}`);
|
||||
}, [safeNavigate, urlQuery]);
|
||||
|
||||
// onQueryCategoryChange handles changes to query category
|
||||
// in state as well as sets additional defaults
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import {
|
||||
LoadingOutlined,
|
||||
@@ -285,9 +279,9 @@ function FullView({
|
||||
<Button
|
||||
className="switch-edit-btn"
|
||||
disabled={response.isFetching || response.isLoading}
|
||||
onClick={(e: React.MouseEvent): void => {
|
||||
onClick={(): void => {
|
||||
if (dashboardEditView) {
|
||||
safeNavigate(dashboardEditView, { event: e });
|
||||
safeNavigate(dashboardEditView);
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useMutation, useQuery } from 'react-query';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button, Popover } from 'antd';
|
||||
@@ -28,7 +28,6 @@ import { UserPreference } from 'types/api/preferences/preference';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
import { isIngestionActive } from 'utils/app';
|
||||
import { navigateToPage } from 'utils/navigation';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
|
||||
import AlertRules from './AlertRules/AlertRules';
|
||||
@@ -414,12 +413,12 @@ export default function Home(): JSX.Element {
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className="active-ingestion-card-actions"
|
||||
onClick={(e: React.MouseEvent): void => {
|
||||
onClick={(): void => {
|
||||
// eslint-disable-next-line sonarjs/no-duplicate-string
|
||||
logEvent('Homepage: Ingestion Active Explore clicked', {
|
||||
source: 'Logs',
|
||||
});
|
||||
navigateToPage(ROUTES.LOGS_EXPLORER, history.push, e);
|
||||
history.push(ROUTES.LOGS_EXPLORER);
|
||||
}}
|
||||
onKeyDown={(e): void => {
|
||||
if (e.key === 'Enter') {
|
||||
@@ -456,11 +455,11 @@ export default function Home(): JSX.Element {
|
||||
className="active-ingestion-card-actions"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={(e: React.MouseEvent): void => {
|
||||
onClick={(): void => {
|
||||
logEvent('Homepage: Ingestion Active Explore clicked', {
|
||||
source: 'Traces',
|
||||
});
|
||||
navigateToPage(ROUTES.TRACES_EXPLORER, history.push, e);
|
||||
history.push(ROUTES.TRACES_EXPLORER);
|
||||
}}
|
||||
onKeyDown={(e): void => {
|
||||
if (e.key === 'Enter') {
|
||||
@@ -497,11 +496,11 @@ export default function Home(): JSX.Element {
|
||||
className="active-ingestion-card-actions"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={(e: React.MouseEvent): void => {
|
||||
onClick={(): void => {
|
||||
logEvent('Homepage: Ingestion Active Explore clicked', {
|
||||
source: 'Metrics',
|
||||
});
|
||||
navigateToPage(ROUTES.METRICS_EXPLORER, history.push, e);
|
||||
history.push(ROUTES.METRICS_EXPLORER);
|
||||
}}
|
||||
onKeyDown={(e): void => {
|
||||
if (e.key === 'Enter') {
|
||||
@@ -551,11 +550,11 @@ export default function Home(): JSX.Element {
|
||||
type="default"
|
||||
className="periscope-btn secondary"
|
||||
icon={<Wrench size={14} />}
|
||||
onClick={(e: React.MouseEvent): void => {
|
||||
onClick={(): void => {
|
||||
logEvent('Homepage: Explore clicked', {
|
||||
source: 'Logs',
|
||||
});
|
||||
navigateToPage(ROUTES.LOGS_EXPLORER, history.push, e);
|
||||
history.push(ROUTES.LOGS_EXPLORER);
|
||||
}}
|
||||
>
|
||||
Open Logs Explorer
|
||||
@@ -565,11 +564,11 @@ export default function Home(): JSX.Element {
|
||||
type="default"
|
||||
className="periscope-btn secondary"
|
||||
icon={<Wrench size={14} />}
|
||||
onClick={(e: React.MouseEvent): void => {
|
||||
onClick={(): void => {
|
||||
logEvent('Homepage: Explore clicked', {
|
||||
source: 'Traces',
|
||||
});
|
||||
navigateToPage(ROUTES.TRACES_EXPLORER, history.push, e);
|
||||
history.push(ROUTES.TRACES_EXPLORER);
|
||||
}}
|
||||
>
|
||||
Open Traces Explorer
|
||||
@@ -579,11 +578,11 @@ export default function Home(): JSX.Element {
|
||||
type="default"
|
||||
className="periscope-btn secondary"
|
||||
icon={<Wrench size={14} />}
|
||||
onClick={(e: React.MouseEvent): void => {
|
||||
onClick={(): void => {
|
||||
logEvent('Homepage: Explore clicked', {
|
||||
source: 'Metrics',
|
||||
});
|
||||
navigateToPage(ROUTES.METRICS_EXPLORER_EXPLORER, history.push, e);
|
||||
history.push(ROUTES.METRICS_EXPLORER_EXPLORER);
|
||||
}}
|
||||
>
|
||||
Open Metrics Explorer
|
||||
@@ -620,11 +619,11 @@ export default function Home(): JSX.Element {
|
||||
type="default"
|
||||
className="periscope-btn secondary"
|
||||
icon={<Plus size={14} />}
|
||||
onClick={(e: React.MouseEvent): void => {
|
||||
onClick={(): void => {
|
||||
logEvent('Homepage: Explore clicked', {
|
||||
source: 'Dashboards',
|
||||
});
|
||||
navigateToPage(ROUTES.ALL_DASHBOARD, history.push, e);
|
||||
history.push(ROUTES.ALL_DASHBOARD);
|
||||
}}
|
||||
>
|
||||
Create dashboard
|
||||
@@ -662,11 +661,11 @@ export default function Home(): JSX.Element {
|
||||
type="default"
|
||||
className="periscope-btn secondary"
|
||||
icon={<Plus size={14} />}
|
||||
onClick={(e: React.MouseEvent): void => {
|
||||
onClick={(): void => {
|
||||
logEvent('Homepage: Explore clicked', {
|
||||
source: 'Alerts',
|
||||
});
|
||||
navigateToPage(ROUTES.ALERTS_NEW, history.push, e);
|
||||
history.push(ROUTES.ALERTS_NEW);
|
||||
}}
|
||||
>
|
||||
Create an alert
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { QueryKey } from 'react-query';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Link } from 'react-router-dom';
|
||||
@@ -116,7 +116,7 @@ const ServicesListTable = memo(
|
||||
onRowClick,
|
||||
}: {
|
||||
services: ServicesList[];
|
||||
onRowClick: (record: ServicesList, event: React.MouseEvent) => void;
|
||||
onRowClick: (record: ServicesList) => void;
|
||||
}): JSX.Element => (
|
||||
<div className="services-list-container home-data-item-container metrics-services-list">
|
||||
<div className="services-list">
|
||||
@@ -125,8 +125,8 @@ const ServicesListTable = memo(
|
||||
dataSource={services}
|
||||
pagination={false}
|
||||
className="services-table"
|
||||
onRow={(record: ServicesList): Record<string, unknown> => ({
|
||||
onClick: (event: React.MouseEvent): void => onRowClick(record, event),
|
||||
onRow={(record): { onClick: () => void } => ({
|
||||
onClick: (): void => onRowClick(record),
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
@@ -284,11 +284,11 @@ function ServiceMetrics({
|
||||
}, [onUpdateChecklistDoneItem, loadingUserPreferences, servicesExist]);
|
||||
|
||||
const handleRowClick = useCallback(
|
||||
(record: ServicesList, event: React.MouseEvent) => {
|
||||
(record: ServicesList) => {
|
||||
logEvent('Homepage: Service clicked', {
|
||||
serviceName: record.serviceName,
|
||||
});
|
||||
safeNavigate(`${ROUTES.APPLICATION}/${record.serviceName}`, { event });
|
||||
safeNavigate(`${ROUTES.APPLICATION}/${record.serviceName}`);
|
||||
},
|
||||
[safeNavigate],
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Button, Select, Skeleton, Table } from 'antd';
|
||||
@@ -172,13 +172,13 @@ export default function ServiceTraces({
|
||||
dataSource={top5Services}
|
||||
pagination={false}
|
||||
className="services-table"
|
||||
onRow={(record: ServicesList): Record<string, unknown> => ({
|
||||
onClick: (event: React.MouseEvent): void => {
|
||||
onRow={(record): { onClick: () => void } => ({
|
||||
onClick: (): void => {
|
||||
logEvent('Homepage: Service clicked', {
|
||||
serviceName: record.serviceName,
|
||||
});
|
||||
|
||||
safeNavigate(`${ROUTES.APPLICATION}/${record.serviceName}`, { event });
|
||||
safeNavigate(`${ROUTES.APPLICATION}/${record.serviceName}`);
|
||||
},
|
||||
})}
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
Skeleton,
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
import { SorterResult } from 'antd/es/table/interface';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
|
||||
|
||||
import HostsEmptyOrIncorrectMetrics from './HostsEmptyOrIncorrectMetrics';
|
||||
import {
|
||||
@@ -77,16 +76,7 @@ export default function HostsListTable({
|
||||
[],
|
||||
);
|
||||
|
||||
const handleRowClick = (
|
||||
record: HostRowData,
|
||||
event: React.MouseEvent,
|
||||
): void => {
|
||||
if (isModifierKeyPressed(event)) {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
params.set('hostName', record.hostName);
|
||||
openInNewTab(`${window.location.pathname}?${params.toString()}`);
|
||||
return;
|
||||
}
|
||||
const handleRowClick = (record: HostRowData): void => {
|
||||
onHostClick(record.hostName);
|
||||
logEvent(InfraMonitoringEvents.ItemClicked, {
|
||||
entity: InfraMonitoringEvents.HostEntity,
|
||||
@@ -190,8 +180,8 @@ export default function HostsListTable({
|
||||
tableLayout="fixed"
|
||||
rowKey={(record): string => record.hostName}
|
||||
onChange={handleTableChange}
|
||||
onRow={(record: HostRowData): Record<string, unknown> => ({
|
||||
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => handleRowClick(record),
|
||||
className: 'clickable-row',
|
||||
})}
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
@@ -24,7 +24,6 @@ import { ChevronDown, ChevronRight } from 'lucide-react';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
|
||||
|
||||
import { FeatureKeys } from '../../../constants/features';
|
||||
import { useAppContext } from '../../../providers/App/App';
|
||||
@@ -452,19 +451,7 @@ function K8sClustersList({
|
||||
);
|
||||
}, [selectedClusterName, groupBy.length, clustersData, nestedClustersData]);
|
||||
|
||||
const handleRowClick = (
|
||||
record: K8sClustersRowData,
|
||||
event: React.MouseEvent,
|
||||
): void => {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
newParams.set(
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS.CLUSTER_NAME,
|
||||
record.clusterUID,
|
||||
);
|
||||
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
|
||||
return;
|
||||
}
|
||||
const handleRowClick = (record: K8sClustersRowData): void => {
|
||||
if (groupBy.length === 0) {
|
||||
setSelectedRowData(null);
|
||||
setselectedClusterName(record.clusterUID);
|
||||
@@ -528,19 +515,8 @@ function K8sClustersList({
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
}}
|
||||
showHeader={false}
|
||||
onRow={(
|
||||
record,
|
||||
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
|
||||
onClick: (event: React.MouseEvent): void => {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
newParams.set(
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS.CLUSTER_NAME,
|
||||
record.clusterUID,
|
||||
);
|
||||
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
|
||||
return;
|
||||
}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => {
|
||||
setselectedClusterName(record.clusterUID);
|
||||
},
|
||||
className: 'expanded-clickable-row',
|
||||
@@ -731,10 +707,8 @@ function K8sClustersList({
|
||||
}}
|
||||
tableLayout="fixed"
|
||||
onChange={handleTableChange}
|
||||
onRow={(
|
||||
record,
|
||||
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
|
||||
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => handleRowClick(record),
|
||||
className: 'clickable-row',
|
||||
})}
|
||||
expandable={{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
@@ -25,7 +25,6 @@ import { ChevronDown, ChevronRight } from 'lucide-react';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
|
||||
|
||||
import { FeatureKeys } from '../../../constants/features';
|
||||
import { useAppContext } from '../../../providers/App/App';
|
||||
@@ -458,19 +457,7 @@ function K8sDaemonSetsList({
|
||||
nestedDaemonSetsData,
|
||||
]);
|
||||
|
||||
const handleRowClick = (
|
||||
record: K8sDaemonSetsRowData,
|
||||
event?: React.MouseEvent,
|
||||
): void => {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
newParams.set(
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS.DAEMONSET_UID,
|
||||
record.daemonsetUID,
|
||||
);
|
||||
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
|
||||
return;
|
||||
}
|
||||
const handleRowClick = (record: K8sDaemonSetsRowData): void => {
|
||||
if (groupBy.length === 0) {
|
||||
setSelectedRowData(null);
|
||||
setSelectedDaemonSetUID(record.daemonsetUID);
|
||||
@@ -534,19 +521,8 @@ function K8sDaemonSetsList({
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
}}
|
||||
showHeader={false}
|
||||
onRow={(
|
||||
record,
|
||||
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
|
||||
onClick: (event: React.MouseEvent): void => {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
newParams.set(
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS.DAEMONSET_UID,
|
||||
record.daemonsetUID,
|
||||
);
|
||||
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
|
||||
return;
|
||||
}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => {
|
||||
setSelectedDaemonSetUID(record.daemonsetUID);
|
||||
},
|
||||
className: 'expanded-clickable-row',
|
||||
@@ -739,10 +715,8 @@ function K8sDaemonSetsList({
|
||||
}}
|
||||
tableLayout="fixed"
|
||||
onChange={handleTableChange}
|
||||
onRow={(
|
||||
record,
|
||||
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
|
||||
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => handleRowClick(record),
|
||||
className: 'clickable-row',
|
||||
})}
|
||||
expandable={{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
@@ -25,7 +25,6 @@ import { ChevronDown, ChevronRight } from 'lucide-react';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
|
||||
|
||||
import { FeatureKeys } from '../../../constants/features';
|
||||
import { useAppContext } from '../../../providers/App/App';
|
||||
@@ -464,19 +463,7 @@ function K8sDeploymentsList({
|
||||
nestedDeploymentsData,
|
||||
]);
|
||||
|
||||
const handleRowClick = (
|
||||
record: K8sDeploymentsRowData,
|
||||
event?: React.MouseEvent,
|
||||
): void => {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
newParams.set(
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS.DEPLOYMENT_UID,
|
||||
record.deploymentUID,
|
||||
);
|
||||
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
|
||||
return;
|
||||
}
|
||||
const handleRowClick = (record: K8sDeploymentsRowData): void => {
|
||||
if (groupBy.length === 0) {
|
||||
setSelectedRowData(null);
|
||||
setselectedDeploymentUID(record.deploymentUID);
|
||||
@@ -540,19 +527,8 @@ function K8sDeploymentsList({
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
}}
|
||||
showHeader={false}
|
||||
onRow={(
|
||||
record,
|
||||
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
|
||||
onClick: (event: React.MouseEvent): void => {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
newParams.set(
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS.DEPLOYMENT_UID,
|
||||
record.deploymentUID,
|
||||
);
|
||||
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
|
||||
return;
|
||||
}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => {
|
||||
setselectedDeploymentUID(record.deploymentUID);
|
||||
},
|
||||
className: 'expanded-clickable-row',
|
||||
@@ -746,10 +722,8 @@ function K8sDeploymentsList({
|
||||
}}
|
||||
tableLayout="fixed"
|
||||
onChange={handleTableChange}
|
||||
onRow={(
|
||||
record,
|
||||
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
|
||||
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => handleRowClick(record),
|
||||
className: 'clickable-row',
|
||||
})}
|
||||
expandable={{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
@@ -25,7 +25,6 @@ import { ChevronDown, ChevronRight } from 'lucide-react';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
|
||||
|
||||
import { FeatureKeys } from '../../../constants/features';
|
||||
import { useAppContext } from '../../../providers/App/App';
|
||||
@@ -429,16 +428,7 @@ function K8sJobsList({
|
||||
return jobsData.find((job) => job.jobName === selectedJobUID) || null;
|
||||
}, [selectedJobUID, groupBy.length, jobsData, nestedJobsData]);
|
||||
|
||||
const handleRowClick = (
|
||||
record: K8sJobsRowData,
|
||||
event: React.MouseEvent,
|
||||
): void => {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
newParams.set(INFRA_MONITORING_K8S_PARAMS_KEYS.JOB_UID, record.jobUID);
|
||||
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
|
||||
return;
|
||||
}
|
||||
const handleRowClick = (record: K8sJobsRowData): void => {
|
||||
if (groupBy.length === 0) {
|
||||
setSelectedRowData(null);
|
||||
setselectedJobUID(record.jobUID);
|
||||
@@ -502,16 +492,8 @@ function K8sJobsList({
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
}}
|
||||
showHeader={false}
|
||||
onRow={(
|
||||
record,
|
||||
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
|
||||
onClick: (event: React.MouseEvent): void => {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
newParams.set(INFRA_MONITORING_K8S_PARAMS_KEYS.JOB_UID, record.jobUID);
|
||||
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
|
||||
return;
|
||||
}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => {
|
||||
setselectedJobUID(record.jobUID);
|
||||
},
|
||||
className: 'expanded-clickable-row',
|
||||
@@ -702,10 +684,8 @@ function K8sJobsList({
|
||||
}}
|
||||
tableLayout="fixed"
|
||||
onChange={handleTableChange}
|
||||
onRow={(
|
||||
record,
|
||||
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
|
||||
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => handleRowClick(record),
|
||||
className: 'clickable-row',
|
||||
})}
|
||||
expandable={{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
@@ -24,7 +24,6 @@ import { ChevronDown, ChevronRight } from 'lucide-react';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
|
||||
|
||||
import { FeatureKeys } from '../../../constants/features';
|
||||
import { useAppContext } from '../../../providers/App/App';
|
||||
@@ -460,19 +459,7 @@ function K8sNamespacesList({
|
||||
nestedNamespacesData,
|
||||
]);
|
||||
|
||||
const handleRowClick = (
|
||||
record: K8sNamespacesRowData,
|
||||
event: React.MouseEvent,
|
||||
): void => {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
newParams.set(
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS.NAMESPACE_UID,
|
||||
record.namespaceUID,
|
||||
);
|
||||
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
|
||||
return;
|
||||
}
|
||||
const handleRowClick = (record: K8sNamespacesRowData): void => {
|
||||
if (groupBy.length === 0) {
|
||||
setSelectedRowData(null);
|
||||
setselectedNamespaceUID(record.namespaceUID);
|
||||
@@ -536,19 +523,8 @@ function K8sNamespacesList({
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
}}
|
||||
showHeader={false}
|
||||
onRow={(
|
||||
record,
|
||||
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
|
||||
onClick: (event: React.MouseEvent): void => {
|
||||
if (isModifierKeyPressed(event)) {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
newParams.set(
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS.NAMESPACE_UID,
|
||||
record.namespaceUID,
|
||||
);
|
||||
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
|
||||
return;
|
||||
}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => {
|
||||
setselectedNamespaceUID(record.namespaceUID);
|
||||
},
|
||||
className: 'expanded-clickable-row',
|
||||
@@ -740,10 +716,8 @@ function K8sNamespacesList({
|
||||
}}
|
||||
tableLayout="fixed"
|
||||
onChange={handleTableChange}
|
||||
onRow={(
|
||||
record,
|
||||
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
|
||||
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => handleRowClick(record),
|
||||
className: 'clickable-row',
|
||||
})}
|
||||
expandable={{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
@@ -24,7 +24,6 @@ import { ChevronDown, ChevronRight } from 'lucide-react';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
|
||||
|
||||
import { FeatureKeys } from '../../../constants/features';
|
||||
import { useAppContext } from '../../../providers/App/App';
|
||||
@@ -439,16 +438,7 @@ function K8sNodesList({
|
||||
return nodesData.find((node) => node.nodeUID === selectedNodeUID) || null;
|
||||
}, [selectedNodeUID, groupBy.length, nodesData, nestedNodesData]);
|
||||
|
||||
const handleRowClick = (
|
||||
record: K8sNodesRowData,
|
||||
event: React.MouseEvent,
|
||||
): void => {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
newParams.set(INFRA_MONITORING_K8S_PARAMS_KEYS.NODE_UID, record.nodeUID);
|
||||
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
|
||||
return;
|
||||
}
|
||||
const handleRowClick = (record: K8sNodesRowData): void => {
|
||||
if (groupBy.length === 0) {
|
||||
setSelectedRowData(null);
|
||||
setSelectedNodeUID(record.nodeUID);
|
||||
@@ -513,19 +503,8 @@ function K8sNodesList({
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
}}
|
||||
showHeader={false}
|
||||
onRow={(
|
||||
record,
|
||||
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
|
||||
onClick: (event: React.MouseEvent): void => {
|
||||
if (isModifierKeyPressed(event)) {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
newParams.set(
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS.NODE_UID,
|
||||
record.nodeUID,
|
||||
);
|
||||
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
|
||||
return;
|
||||
}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => {
|
||||
setSelectedNodeUID(record.nodeUID);
|
||||
},
|
||||
className: 'expanded-clickable-row',
|
||||
@@ -716,10 +695,8 @@ function K8sNodesList({
|
||||
}}
|
||||
tableLayout="fixed"
|
||||
onChange={handleTableChange}
|
||||
onRow={(
|
||||
record,
|
||||
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
|
||||
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => handleRowClick(record),
|
||||
className: 'clickable-row',
|
||||
})}
|
||||
expandable={{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
@@ -26,7 +26,6 @@ import { ChevronDown, ChevronRight, CornerDownRight } from 'lucide-react';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
|
||||
|
||||
import { FeatureKeys } from '../../../constants/features';
|
||||
import { useAppContext } from '../../../providers/App/App';
|
||||
@@ -496,16 +495,7 @@ function K8sPodsList({
|
||||
}
|
||||
}, [selectedRowData, fetchGroupedByRowData]);
|
||||
|
||||
const handleRowClick = (
|
||||
record: K8sPodsRowData,
|
||||
event: React.MouseEvent,
|
||||
): void => {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
newParams.set(INFRA_MONITORING_K8S_PARAMS_KEYS.POD_UID, record.podUID);
|
||||
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
|
||||
return;
|
||||
}
|
||||
const handleRowClick = (record: K8sPodsRowData): void => {
|
||||
if (groupBy.length === 0) {
|
||||
setSelectedPodUID(record.podUID);
|
||||
setSearchParams({
|
||||
@@ -625,14 +615,8 @@ function K8sPodsList({
|
||||
spinning: isFetchingGroupedByRowData || isLoadingGroupedByRowData,
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
}}
|
||||
onRow={(record: K8sPodsRowData): Record<string, unknown> => ({
|
||||
onClick: (event: React.MouseEvent): void => {
|
||||
if (isModifierKeyPressed(event)) {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
newParams.set(INFRA_MONITORING_K8S_PARAMS_KEYS.POD_UID, record.podUID);
|
||||
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
|
||||
return;
|
||||
}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => {
|
||||
setSelectedPodUID(record.podUID);
|
||||
},
|
||||
className: 'expanded-clickable-row',
|
||||
@@ -768,8 +752,8 @@ function K8sPodsList({
|
||||
scroll={{ x: true }}
|
||||
tableLayout="fixed"
|
||||
onChange={handleTableChange}
|
||||
onRow={(record: K8sPodsRowData): Record<string, unknown> => ({
|
||||
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => handleRowClick(record),
|
||||
className: 'clickable-row',
|
||||
})}
|
||||
expandable={{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
@@ -25,7 +25,6 @@ import { ChevronDown, ChevronRight } from 'lucide-react';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
|
||||
|
||||
import { FeatureKeys } from '../../../constants/features';
|
||||
import { useAppContext } from '../../../providers/App/App';
|
||||
@@ -461,19 +460,7 @@ function K8sStatefulSetsList({
|
||||
nestedStatefulSetsData,
|
||||
]);
|
||||
|
||||
const handleRowClick = (
|
||||
record: K8sStatefulSetsRowData,
|
||||
event: React.MouseEvent,
|
||||
): void => {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
newParams.set(
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS.STATEFULSET_UID,
|
||||
record.statefulsetUID,
|
||||
);
|
||||
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
|
||||
return;
|
||||
}
|
||||
const handleRowClick = (record: K8sStatefulSetsRowData): void => {
|
||||
if (groupBy.length === 0) {
|
||||
setSelectedRowData(null);
|
||||
setselectedStatefulSetUID(record.statefulsetUID);
|
||||
@@ -537,19 +524,8 @@ function K8sStatefulSetsList({
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
}}
|
||||
showHeader={false}
|
||||
onRow={(
|
||||
record,
|
||||
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
|
||||
onClick: (event: React.MouseEvent): void => {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
newParams.set(
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS.STATEFULSET_UID,
|
||||
record.statefulsetUID,
|
||||
);
|
||||
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
|
||||
return;
|
||||
}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => {
|
||||
setselectedStatefulSetUID(record.statefulsetUID);
|
||||
},
|
||||
className: 'expanded-clickable-row',
|
||||
@@ -742,8 +718,8 @@ function K8sStatefulSetsList({
|
||||
}}
|
||||
tableLayout="fixed"
|
||||
onChange={handleTableChange}
|
||||
onRow={(record) => ({
|
||||
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => handleRowClick(record),
|
||||
className: 'clickable-row',
|
||||
})}
|
||||
expandable={{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
@@ -25,7 +25,6 @@ import { ChevronDown, ChevronRight } from 'lucide-react';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
|
||||
|
||||
import { FeatureKeys } from '../../../constants/features';
|
||||
import { useAppContext } from '../../../providers/App/App';
|
||||
@@ -391,16 +390,7 @@ function K8sVolumesList({
|
||||
);
|
||||
}, [selectedVolumeUID, volumesData, groupBy.length, nestedVolumesData]);
|
||||
|
||||
const handleRowClick = (
|
||||
record: K8sVolumesRowData,
|
||||
event: React.MouseEvent,
|
||||
): void => {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
newParams.set(INFRA_MONITORING_K8S_PARAMS_KEYS.VOLUME_UID, record.volumeUID);
|
||||
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
|
||||
return;
|
||||
}
|
||||
const handleRowClick = (record: K8sVolumesRowData): void => {
|
||||
if (groupBy.length === 0) {
|
||||
setSelectedRowData(null);
|
||||
setselectedVolumeUID(record.volumeUID);
|
||||
@@ -464,19 +454,8 @@ function K8sVolumesList({
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
}}
|
||||
showHeader={false}
|
||||
onRow={(
|
||||
record,
|
||||
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
|
||||
onClick: (event: React.MouseEvent): void => {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
newParams.set(
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS.VOLUME_UID,
|
||||
record.volumeUID,
|
||||
);
|
||||
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
|
||||
return;
|
||||
}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => {
|
||||
setselectedVolumeUID(record.volumeUID);
|
||||
},
|
||||
className: 'expanded-clickable-row',
|
||||
@@ -662,10 +641,8 @@ function K8sVolumesList({
|
||||
}}
|
||||
tableLayout="fixed"
|
||||
onChange={handleTableChange}
|
||||
onRow={(
|
||||
record,
|
||||
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
|
||||
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => handleRowClick(record),
|
||||
className: 'clickable-row',
|
||||
})}
|
||||
expandable={{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import { Button, Divider, Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
@@ -7,7 +7,6 @@ import useComponentPermission from 'hooks/useComponentPermission';
|
||||
import history from 'lib/history';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
|
||||
|
||||
import AlertInfoCard from './AlertInfoCard';
|
||||
import { ALERT_CARDS, ALERT_INFO_LINKS } from './alertLinks';
|
||||
@@ -37,13 +36,9 @@ export function AlertsEmptyState(): JSX.Element {
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const onClickNewAlertHandler = useCallback((e: React.MouseEvent) => {
|
||||
const onClickNewAlertHandler = useCallback(() => {
|
||||
setLoading(false);
|
||||
if (isModifierKeyPressed(e)) {
|
||||
openInNewTab(ROUTES.ALERTS_NEW);
|
||||
} else {
|
||||
history.push(ROUTES.ALERTS_NEW);
|
||||
}
|
||||
history.push(ROUTES.ALERTS_NEW);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable react/display-name */
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { UseQueryResult } from 'react-query';
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
@@ -100,22 +100,16 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
||||
});
|
||||
}, [notificationsApi, t]);
|
||||
|
||||
const onClickNewAlertHandler = useCallback(
|
||||
(e: React.MouseEvent): void => {
|
||||
logEvent('Alert: New alert button clicked', {
|
||||
number: allAlertRules?.length,
|
||||
layout: 'new',
|
||||
});
|
||||
safeNavigate(ROUTES.ALERT_TYPE_SELECTION, { event: e });
|
||||
},
|
||||
const onClickNewAlertHandler = useCallback(() => {
|
||||
logEvent('Alert: New alert button clicked', {
|
||||
number: allAlertRules?.length,
|
||||
layout: 'new',
|
||||
});
|
||||
safeNavigate(ROUTES.ALERT_TYPE_SELECTION);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[],
|
||||
);
|
||||
}, []);
|
||||
|
||||
const onEditHandler = (
|
||||
record: GettableAlert,
|
||||
options?: { event?: React.MouseEvent; newTab?: boolean },
|
||||
): void => {
|
||||
const onEditHandler = (record: GettableAlert, openInNewTab: boolean): void => {
|
||||
const compositeQuery = sanitizeDefaultAlertQuery(
|
||||
mapQueryDataFromApi(record.condition.compositeQuery),
|
||||
record.alertType as AlertTypes,
|
||||
@@ -131,10 +125,11 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
||||
|
||||
setEditLoader(false);
|
||||
|
||||
safeNavigate(`${ROUTES.ALERT_OVERVIEW}?${params.toString()}`, {
|
||||
event: options?.event,
|
||||
newTab: options?.newTab,
|
||||
});
|
||||
if (openInNewTab) {
|
||||
window.open(`${ROUTES.ALERT_OVERVIEW}?${params.toString()}`, '_blank');
|
||||
} else {
|
||||
safeNavigate(`${ROUTES.ALERT_OVERVIEW}?${params.toString()}`);
|
||||
}
|
||||
};
|
||||
|
||||
const onCloneHandler = (
|
||||
@@ -271,7 +266,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
||||
const onClickHandler = (e: React.MouseEvent<HTMLElement>): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
onEditHandler(record, { event: e });
|
||||
onEditHandler(record, e.metaKey || e.ctrlKey);
|
||||
};
|
||||
|
||||
return <Typography.Link onClick={onClickHandler}>{value}</Typography.Link>;
|
||||
@@ -336,9 +331,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
||||
/>,
|
||||
<ColumnButton
|
||||
key="2"
|
||||
onClick={(e: React.MouseEvent): void =>
|
||||
onEditHandler(record, { event: e })
|
||||
}
|
||||
onClick={(): void => onEditHandler(record, false)}
|
||||
type="link"
|
||||
loading={editLoader}
|
||||
>
|
||||
@@ -346,7 +339,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
||||
</ColumnButton>,
|
||||
<ColumnButton
|
||||
key="3"
|
||||
onClick={(): void => onEditHandler(record, { newTab: true })}
|
||||
onClick={(): void => onEditHandler(record, true)}
|
||||
type="link"
|
||||
loading={editLoader}
|
||||
>
|
||||
|
||||
@@ -416,7 +416,11 @@ function DashboardsList(): JSX.Element {
|
||||
|
||||
const onClickHandler = (event: React.MouseEvent<HTMLElement>): void => {
|
||||
event.stopPropagation();
|
||||
safeNavigate(getLink(), { event });
|
||||
if (event.metaKey || event.ctrlKey) {
|
||||
window.open(getLink(), '_blank');
|
||||
} else {
|
||||
safeNavigate(getLink());
|
||||
}
|
||||
logEvent('Dashboard List: Clicked on dashboard', {
|
||||
dashboardId: dashboard.id,
|
||||
dashboardName: dashboard.name,
|
||||
|
||||
@@ -31,7 +31,6 @@ import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
|
||||
import { secondsToMilliseconds } from 'utils/timeUtils';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
@@ -236,7 +235,7 @@ function Application(): JSX.Element {
|
||||
timestamp: number,
|
||||
apmToTraceQuery: Query,
|
||||
isViewLogsClicked?: boolean,
|
||||
): ((e: React.MouseEvent) => void) => (e: React.MouseEvent): void => {
|
||||
): (() => void) => (): void => {
|
||||
const endTime = secondsToMilliseconds(timestamp);
|
||||
const startTime = secondsToMilliseconds(timestamp - stepInterval);
|
||||
|
||||
@@ -260,11 +259,7 @@ function Application(): JSX.Element {
|
||||
queryString,
|
||||
);
|
||||
|
||||
if (isModifierKeyPressed(e)) {
|
||||
openInNewTab(newPath);
|
||||
} else {
|
||||
history.push(newPath);
|
||||
}
|
||||
history.push(newPath);
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[stepInterval],
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from 'react';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button } from 'antd';
|
||||
import { Binoculars, DraftingCompass, ScrollText } from 'lucide-react';
|
||||
@@ -7,9 +6,9 @@ import './GraphControlsPanel.styles.scss';
|
||||
|
||||
interface GraphControlsPanelProps {
|
||||
id: string;
|
||||
onViewLogsClick?: (e: React.MouseEvent) => void;
|
||||
onViewTracesClick: (e: React.MouseEvent) => void;
|
||||
onViewAPIMonitoringClick?: (e: React.MouseEvent) => void;
|
||||
onViewLogsClick?: () => void;
|
||||
onViewTracesClick: () => void;
|
||||
onViewAPIMonitoringClick?: () => void;
|
||||
}
|
||||
|
||||
function GraphControlsPanel({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { Dispatch, SetStateAction, useMemo, useRef } from 'react';
|
||||
import { Dispatch, SetStateAction, useMemo, useRef } from 'react';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
@@ -42,10 +42,7 @@ interface OnViewTracePopupClickProps {
|
||||
apmToTraceQuery: Query;
|
||||
isViewLogsClicked?: boolean;
|
||||
stepInterval?: number;
|
||||
safeNavigate: (
|
||||
url: string,
|
||||
options?: { event?: React.MouseEvent | MouseEvent },
|
||||
) => void;
|
||||
safeNavigate: (url: string) => void;
|
||||
}
|
||||
|
||||
interface OnViewAPIMonitoringPopupClickProps {
|
||||
@@ -54,10 +51,8 @@ interface OnViewAPIMonitoringPopupClickProps {
|
||||
stepInterval?: number;
|
||||
domainName: string;
|
||||
isError: boolean;
|
||||
safeNavigate: (
|
||||
url: string,
|
||||
options?: { event?: React.MouseEvent | MouseEvent },
|
||||
) => void;
|
||||
|
||||
safeNavigate: (url: string) => void;
|
||||
}
|
||||
|
||||
export function generateExplorerPath(
|
||||
@@ -98,8 +93,8 @@ export function onViewTracePopupClick({
|
||||
isViewLogsClicked,
|
||||
stepInterval,
|
||||
safeNavigate,
|
||||
}: OnViewTracePopupClickProps): (e?: React.MouseEvent) => void {
|
||||
return (e?: React.MouseEvent): void => {
|
||||
}: OnViewTracePopupClickProps): VoidFunction {
|
||||
return (): void => {
|
||||
const endTime = secondsToMilliseconds(timestamp);
|
||||
const startTime = secondsToMilliseconds(timestamp - (stepInterval || 60));
|
||||
|
||||
@@ -123,7 +118,7 @@ export function onViewTracePopupClick({
|
||||
queryString,
|
||||
);
|
||||
|
||||
safeNavigate(newPath, { event: e });
|
||||
safeNavigate(newPath);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -154,8 +149,8 @@ export function onViewAPIMonitoringPopupClick({
|
||||
isError,
|
||||
stepInterval,
|
||||
safeNavigate,
|
||||
}: OnViewAPIMonitoringPopupClickProps): (e?: React.MouseEvent) => void {
|
||||
return (e?: React.MouseEvent): void => {
|
||||
}: OnViewAPIMonitoringPopupClickProps): VoidFunction {
|
||||
return (): void => {
|
||||
const endTime = timestamp + (stepInterval || 60);
|
||||
const startTime = timestamp - (stepInterval || 60);
|
||||
const filters = {
|
||||
@@ -195,7 +190,7 @@ export function onViewAPIMonitoringPopupClick({
|
||||
filters,
|
||||
);
|
||||
|
||||
safeNavigate(newPath, { event: e });
|
||||
safeNavigate(newPath);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import history from 'lib/history';
|
||||
import { Bell, Grid } from 'lucide-react';
|
||||
import { navigateToPage } from 'utils/navigation';
|
||||
|
||||
import { DashboardsAndAlertsPopoverProps } from './types';
|
||||
|
||||
@@ -26,10 +25,9 @@ function DashboardsAndAlertsPopover({
|
||||
label: (
|
||||
<Typography.Link
|
||||
key={alert.alert_id}
|
||||
onClick={(e): void => {
|
||||
onClick={(): void => {
|
||||
params.set(QueryParams.ruleId, alert.alert_id);
|
||||
const path = `${ROUTES.ALERT_OVERVIEW}?${params.toString()}`;
|
||||
navigateToPage(path, history.push, e);
|
||||
history.push(`${ROUTES.ALERT_OVERVIEW}?${params.toString()}`);
|
||||
}}
|
||||
className="dashboards-popover-content-item"
|
||||
>
|
||||
@@ -57,12 +55,11 @@ function DashboardsAndAlertsPopover({
|
||||
label: (
|
||||
<Typography.Link
|
||||
key={dashboard.dashboard_id}
|
||||
onClick={(e): void => {
|
||||
onClick={(): void => {
|
||||
safeNavigate(
|
||||
generatePath(ROUTES.DASHBOARD, {
|
||||
dashboardId: dashboard.dashboard_id,
|
||||
}),
|
||||
{ event: e },
|
||||
);
|
||||
}}
|
||||
className="dashboards-popover-content-item"
|
||||
|
||||
@@ -118,7 +118,6 @@ describe('DashboardsAndAlertsPopover', () => {
|
||||
// Should navigate to the dashboard
|
||||
expect(mockSafeNavigate).toHaveBeenCalledWith(
|
||||
`/dashboard/${mockDashboard1.dashboard_id}`,
|
||||
expect.objectContaining({ event: expect.any(Object) }),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
Spin,
|
||||
@@ -107,9 +107,8 @@ function MetricsTable({
|
||||
onChange: onPaginationChange,
|
||||
total: totalCount,
|
||||
}}
|
||||
onRow={(record: MetricsListItemRowData): Record<string, unknown> => ({
|
||||
onClick: (event: React.MouseEvent): void =>
|
||||
openMetricDetails(record.key, 'list', event),
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => openMetricDetails(record.key, 'list'),
|
||||
className: 'clickable-row',
|
||||
})}
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable no-nested-ternary */
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||
import * as Sentry from '@sentry/react';
|
||||
@@ -14,7 +14,6 @@ import { AppState } from 'store/reducers';
|
||||
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
|
||||
|
||||
import { MetricsExplorerEventKeys, MetricsExplorerEvents } from '../events';
|
||||
import InspectModal from '../Inspect';
|
||||
@@ -210,15 +209,7 @@ function Summary(): JSX.Element {
|
||||
const openMetricDetails = (
|
||||
metricName: string,
|
||||
view: 'list' | 'treemap',
|
||||
event?: React.MouseEvent,
|
||||
): void => {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
newParams.set(IS_METRIC_DETAILS_OPEN_KEY, 'true');
|
||||
newParams.set(SELECTED_METRIC_NAME_KEY, metricName);
|
||||
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
|
||||
return;
|
||||
}
|
||||
setSelectedMetricName(metricName);
|
||||
setIsMetricDetailsOpen(true);
|
||||
setSearchParams({
|
||||
|
||||
@@ -194,11 +194,7 @@ describe('MetricsTable', () => {
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByText('Metric 1'));
|
||||
expect(mockOpenMetricDetails).toHaveBeenCalledWith(
|
||||
'metric1',
|
||||
'list',
|
||||
expect.any(Object),
|
||||
);
|
||||
expect(mockOpenMetricDetails).toHaveBeenCalledWith('metric1', 'list');
|
||||
});
|
||||
|
||||
it('calls setOrderBy when column header is clicked', () => {
|
||||
|
||||
@@ -14,11 +14,7 @@ export interface MetricsTableProps {
|
||||
onPaginationChange: (page: number, pageSize: number) => void;
|
||||
setOrderBy: (orderBy: OrderByPayload) => void;
|
||||
totalCount: number;
|
||||
openMetricDetails: (
|
||||
metricName: string,
|
||||
view: 'list' | 'treemap',
|
||||
event?: React.MouseEvent,
|
||||
) => void;
|
||||
openMetricDetails: (metricName: string, view: 'list' | 'treemap') => void;
|
||||
queryFilters: TagFilter;
|
||||
}
|
||||
|
||||
@@ -32,11 +28,7 @@ export interface MetricsTreemapProps {
|
||||
isLoading: boolean;
|
||||
isError: boolean;
|
||||
viewType: TreemapViewType;
|
||||
openMetricDetails: (
|
||||
metricName: string,
|
||||
view: 'list' | 'treemap',
|
||||
event?: React.MouseEvent,
|
||||
) => void;
|
||||
openMetricDetails: (metricName: string, view: 'list' | 'treemap') => void;
|
||||
setHeatmapView: (value: TreemapViewType) => void;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { Badge, Button } from 'antd';
|
||||
import ROUTES from 'constants/routes';
|
||||
import history from 'lib/history';
|
||||
import { Undo } from 'lucide-react';
|
||||
import { navigateToPage } from 'utils/navigation';
|
||||
|
||||
import { buttonText, RIBBON_STYLES } from './config';
|
||||
|
||||
@@ -22,30 +21,23 @@ function NewExplorerCTA(): JSX.Element | null {
|
||||
[location.pathname],
|
||||
);
|
||||
|
||||
const onClickHandler = useCallback(
|
||||
(e?: React.MouseEvent): void => {
|
||||
let targetPath: string;
|
||||
if (location.pathname === ROUTES.LOGS_EXPLORER) {
|
||||
targetPath = ROUTES.OLD_LOGS_EXPLORER;
|
||||
} else if (location.pathname === ROUTES.TRACE) {
|
||||
targetPath = ROUTES.TRACES_EXPLORER;
|
||||
} else if (location.pathname === ROUTES.OLD_LOGS_EXPLORER) {
|
||||
targetPath = ROUTES.LOGS_EXPLORER;
|
||||
} else if (location.pathname === ROUTES.TRACES_EXPLORER) {
|
||||
targetPath = ROUTES.TRACE;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
navigateToPage(targetPath, history.push, e);
|
||||
},
|
||||
[location.pathname],
|
||||
);
|
||||
const onClickHandler = useCallback((): void => {
|
||||
if (location.pathname === ROUTES.LOGS_EXPLORER) {
|
||||
history.push(ROUTES.OLD_LOGS_EXPLORER);
|
||||
} else if (location.pathname === ROUTES.TRACE) {
|
||||
history.push(ROUTES.TRACES_EXPLORER);
|
||||
} else if (location.pathname === ROUTES.OLD_LOGS_EXPLORER) {
|
||||
history.push(ROUTES.LOGS_EXPLORER);
|
||||
} else if (location.pathname === ROUTES.TRACES_EXPLORER) {
|
||||
history.push(ROUTES.TRACE);
|
||||
}
|
||||
}, [location.pathname]);
|
||||
|
||||
const button = useMemo(
|
||||
() => (
|
||||
<Button
|
||||
icon={<Undo size={16} />}
|
||||
onClick={(e): void => onClickHandler(e)}
|
||||
onClick={onClickHandler}
|
||||
data-testid="newExplorerCTA"
|
||||
type="text"
|
||||
className="periscope-btn link"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQuery } from 'react-query';
|
||||
import { useEffectOnce } from 'react-use';
|
||||
@@ -17,7 +17,6 @@ import { InviteMemberFormValues } from 'container/OrganizationSettings/PendingIn
|
||||
import history from 'lib/history';
|
||||
import { UserPlus } from 'lucide-react';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { navigateToPage } from 'utils/navigation';
|
||||
|
||||
import ModuleStepsContainer from './common/ModuleStepsContainer/ModuleStepsContainer';
|
||||
import { stepsMap } from './constants/stepsConfig';
|
||||
@@ -253,13 +252,9 @@ export default function Onboarding(): JSX.Element {
|
||||
}
|
||||
};
|
||||
|
||||
const handleNext = (e?: React.MouseEvent): void => {
|
||||
const handleNext = (): void => {
|
||||
if (activeStep <= 3) {
|
||||
navigateToPage(
|
||||
moduleRouteMap[selectedModule.id as ModulesMap],
|
||||
history.push,
|
||||
e,
|
||||
);
|
||||
history.push(moduleRouteMap[selectedModule.id as ModulesMap]);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -322,9 +317,9 @@ export default function Onboarding(): JSX.Element {
|
||||
{activeStep === 1 && (
|
||||
<div className="onboarding-page">
|
||||
<div
|
||||
onClick={(e): void => {
|
||||
onClick={(): void => {
|
||||
logEvent('Onboarding V2: Skip Button Clicked', {});
|
||||
navigateToPage(ROUTES.APPLICATION, history.push, e);
|
||||
history.push(ROUTES.APPLICATION);
|
||||
}}
|
||||
className="skip-to-console"
|
||||
>
|
||||
@@ -360,11 +355,7 @@ export default function Onboarding(): JSX.Element {
|
||||
</div>
|
||||
</div>
|
||||
<div className="continue-to-next-step">
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<ArrowRightOutlined />}
|
||||
onClick={(e): void => handleNext(e)}
|
||||
>
|
||||
<Button type="primary" icon={<ArrowRightOutlined />} onClick={handleNext}>
|
||||
{t('get_started')}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -395,16 +386,17 @@ export default function Onboarding(): JSX.Element {
|
||||
{activeStep > 1 && (
|
||||
<div className="stepsContainer">
|
||||
<ModuleStepsContainer
|
||||
onReselectModule={(e?: React.MouseEvent): void => {
|
||||
onReselectModule={(): void => {
|
||||
setCurrent(current - 1);
|
||||
setActiveStep(activeStep - 1);
|
||||
setSelectedModule(useCases.APM);
|
||||
resetProgress();
|
||||
|
||||
const path = isOnboardingV3Enabled
|
||||
? ROUTES.GET_STARTED_WITH_CLOUD
|
||||
: ROUTES.GET_STARTED;
|
||||
navigateToPage(path, history.push, e);
|
||||
if (isOnboardingV3Enabled) {
|
||||
history.push(ROUTES.GET_STARTED_WITH_CLOUD);
|
||||
} else {
|
||||
history.push(ROUTES.GET_STARTED);
|
||||
}
|
||||
}}
|
||||
selectedModule={selectedModule}
|
||||
selectedModuleSteps={selectedModuleSteps}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
@@ -23,7 +23,6 @@ import {
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { Blocks, Check } from 'lucide-react';
|
||||
import { navigateToPage } from 'utils/navigation';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
|
||||
import './DataSource.styles.scss';
|
||||
@@ -142,13 +141,13 @@ export default function DataSource(): JSX.Element {
|
||||
}
|
||||
};
|
||||
|
||||
const goToIntegrationsPage = (e?: React.MouseEvent): void => {
|
||||
const goToIntegrationsPage = (): void => {
|
||||
logEvent('Onboarding V2: Go to integrations', {
|
||||
module: selectedModule?.id,
|
||||
dataSource: selectedDataSource?.name,
|
||||
framework: selectedFramework,
|
||||
});
|
||||
navigateToPage(ROUTES.INTEGRATIONS, history.push, e);
|
||||
history.push(ROUTES.INTEGRATIONS);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -250,7 +249,7 @@ export default function DataSource(): JSX.Element {
|
||||
page which allows more sources of sending data
|
||||
</Typography.Text>
|
||||
<Button
|
||||
onClick={(e): void => goToIntegrationsPage(e)}
|
||||
onClick={goToIntegrationsPage}
|
||||
icon={<Blocks size={14} />}
|
||||
className="navigate-integrations-page-btn"
|
||||
>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||
/* eslint-disable react/jsx-no-comment-textnodes */
|
||||
/* eslint-disable sonarjs/prefer-single-boolean-return */
|
||||
import React, { SetStateAction, useState } from 'react';
|
||||
import { SetStateAction, useState } from 'react';
|
||||
import {
|
||||
ArrowLeftOutlined,
|
||||
ArrowRightOutlined,
|
||||
@@ -19,7 +19,6 @@ import { hasFrameworks } from 'container/OnboardingContainer/utils/dataSourceUti
|
||||
import history from 'lib/history';
|
||||
import { isEmpty, isNull } from 'lodash-es';
|
||||
import { UserPlus } from 'lucide-react';
|
||||
import { navigateToPage } from 'utils/navigation';
|
||||
|
||||
import { useOnboardingContext } from '../../context/OnboardingContext';
|
||||
import {
|
||||
@@ -143,7 +142,7 @@ export default function ModuleStepsContainer({
|
||||
return true;
|
||||
};
|
||||
|
||||
const redirectToModules = (event?: React.MouseEvent): void => {
|
||||
const redirectToModules = (): void => {
|
||||
logEvent('Onboarding V2 Complete', {
|
||||
module: selectedModule.id,
|
||||
dataSource: selectedDataSource?.id,
|
||||
@@ -153,28 +152,26 @@ export default function ModuleStepsContainer({
|
||||
serviceName,
|
||||
});
|
||||
|
||||
let targetPath: string;
|
||||
if (selectedModule.id === ModulesMap.APM) {
|
||||
targetPath = ROUTES.APPLICATION;
|
||||
history.push(ROUTES.APPLICATION);
|
||||
} else if (selectedModule.id === ModulesMap.LogsManagement) {
|
||||
targetPath = ROUTES.LOGS_EXPLORER;
|
||||
history.push(ROUTES.LOGS_EXPLORER);
|
||||
} else if (selectedModule.id === ModulesMap.InfrastructureMonitoring) {
|
||||
targetPath = ROUTES.APPLICATION;
|
||||
history.push(ROUTES.APPLICATION);
|
||||
} else if (selectedModule.id === ModulesMap.AwsMonitoring) {
|
||||
targetPath = ROUTES.APPLICATION;
|
||||
history.push(ROUTES.APPLICATION);
|
||||
} else {
|
||||
targetPath = ROUTES.APPLICATION;
|
||||
history.push(ROUTES.APPLICATION);
|
||||
}
|
||||
navigateToPage(targetPath, history.push, event);
|
||||
};
|
||||
|
||||
const handleNext = (event?: React.MouseEvent): void => {
|
||||
const handleNext = (): void => {
|
||||
const isValid = isValidForm();
|
||||
|
||||
if (isValid) {
|
||||
if (current === lastStepIndex) {
|
||||
resetProgress();
|
||||
redirectToModules(event);
|
||||
redirectToModules();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -382,8 +379,8 @@ export default function ModuleStepsContainer({
|
||||
}
|
||||
};
|
||||
|
||||
const handleLogoClick = (e: React.MouseEvent): void => {
|
||||
navigateToPage('/home', history.push, e);
|
||||
const handleLogoClick = (): void => {
|
||||
history.push('/home');
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -403,7 +400,7 @@ export default function ModuleStepsContainer({
|
||||
style={{ display: 'flex', alignItems: 'center' }}
|
||||
type="default"
|
||||
icon={<LeftCircleOutlined />}
|
||||
onClick={(e): void => onReselectModule(e)}
|
||||
onClick={onReselectModule}
|
||||
>
|
||||
{selectedModule.title}
|
||||
</Button>
|
||||
@@ -473,11 +470,7 @@ export default function ModuleStepsContainer({
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
onClick={(e): void => handleNext(e)}
|
||||
type="primary"
|
||||
icon={<ArrowRightOutlined />}
|
||||
>
|
||||
<Button onClick={handleNext} type="primary" icon={<ArrowRightOutlined />}>
|
||||
{current < lastStepIndex ? 'Continue to next step' : 'Done'}
|
||||
</Button>
|
||||
<LaunchChatSupport
|
||||
|
||||
@@ -21,7 +21,6 @@ import history from 'lib/history';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import { CheckIcon, Goal, UserPlus, X } from 'lucide-react';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { navigateToPage } from 'utils/navigation';
|
||||
|
||||
import OnboardingIngestionDetails from '../IngestionDetails/IngestionDetails';
|
||||
import InviteTeamMembers from '../InviteTeamMembers/InviteTeamMembers';
|
||||
@@ -414,10 +413,7 @@ function OnboardingAddDataSource(): JSX.Element {
|
||||
]);
|
||||
}, [org]);
|
||||
|
||||
const handleUpdateCurrentStep = (
|
||||
step: number,
|
||||
event?: React.MouseEvent,
|
||||
): void => {
|
||||
const handleUpdateCurrentStep = (step: number): void => {
|
||||
setCurrentStep(step);
|
||||
|
||||
if (step === 1) {
|
||||
@@ -447,45 +443,43 @@ function OnboardingAddDataSource(): JSX.Element {
|
||||
...setupStepItemsBase.slice(2),
|
||||
]);
|
||||
} else if (step === 3) {
|
||||
let targetPath: string;
|
||||
switch (selectedDataSource?.module) {
|
||||
case 'apm':
|
||||
targetPath = ROUTES.APPLICATION;
|
||||
history.push(ROUTES.APPLICATION);
|
||||
break;
|
||||
case 'logs':
|
||||
targetPath = ROUTES.LOGS;
|
||||
history.push(ROUTES.LOGS);
|
||||
break;
|
||||
case 'metrics':
|
||||
targetPath = ROUTES.METRICS_EXPLORER;
|
||||
history.push(ROUTES.METRICS_EXPLORER);
|
||||
break;
|
||||
case 'dashboards':
|
||||
targetPath = ROUTES.ALL_DASHBOARD;
|
||||
history.push(ROUTES.ALL_DASHBOARD);
|
||||
break;
|
||||
case 'infra-monitoring-hosts':
|
||||
targetPath = ROUTES.INFRASTRUCTURE_MONITORING_HOSTS;
|
||||
history.push(ROUTES.INFRASTRUCTURE_MONITORING_HOSTS);
|
||||
break;
|
||||
case 'infra-monitoring-k8s':
|
||||
targetPath = ROUTES.INFRASTRUCTURE_MONITORING_KUBERNETES;
|
||||
history.push(ROUTES.INFRASTRUCTURE_MONITORING_KUBERNETES);
|
||||
break;
|
||||
case 'messaging-queues-kafka':
|
||||
targetPath = ROUTES.MESSAGING_QUEUES_KAFKA;
|
||||
history.push(ROUTES.MESSAGING_QUEUES_KAFKA);
|
||||
break;
|
||||
case 'messaging-queues-celery':
|
||||
targetPath = ROUTES.MESSAGING_QUEUES_CELERY_TASK;
|
||||
history.push(ROUTES.MESSAGING_QUEUES_CELERY_TASK);
|
||||
break;
|
||||
case 'integrations':
|
||||
targetPath = ROUTES.INTEGRATIONS;
|
||||
history.push(ROUTES.INTEGRATIONS);
|
||||
break;
|
||||
case 'home':
|
||||
targetPath = ROUTES.HOME;
|
||||
history.push(ROUTES.HOME);
|
||||
break;
|
||||
case 'api-monitoring':
|
||||
targetPath = ROUTES.API_MONITORING;
|
||||
history.push(ROUTES.API_MONITORING);
|
||||
break;
|
||||
default:
|
||||
targetPath = ROUTES.APPLICATION;
|
||||
history.push(ROUTES.APPLICATION);
|
||||
}
|
||||
navigateToPage(targetPath, history.push, event);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -634,7 +628,7 @@ function OnboardingAddDataSource(): JSX.Element {
|
||||
<X
|
||||
size={14}
|
||||
className="onboarding-header-container-close-icon"
|
||||
onClick={(e): void => {
|
||||
onClick={(): void => {
|
||||
logEvent(
|
||||
`${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.BASE}: ${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.CLOSE_ONBOARDING_CLICKED}`,
|
||||
{
|
||||
@@ -642,7 +636,7 @@ function OnboardingAddDataSource(): JSX.Element {
|
||||
},
|
||||
);
|
||||
|
||||
navigateToPage(ROUTES.HOME, history.push, e);
|
||||
history.push(ROUTES.HOME);
|
||||
}}
|
||||
/>
|
||||
<Typography.Text>Get Started (2/4)</Typography.Text>
|
||||
@@ -969,7 +963,7 @@ function OnboardingAddDataSource(): JSX.Element {
|
||||
type="primary"
|
||||
disabled={!selectedDataSource}
|
||||
shape="round"
|
||||
onClick={(e): void => {
|
||||
onClick={(): void => {
|
||||
logEvent(
|
||||
`${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.BASE}: ${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.CONFIGURED_PRODUCT}`,
|
||||
{
|
||||
@@ -983,7 +977,7 @@ function OnboardingAddDataSource(): JSX.Element {
|
||||
selectedEnvironment || selectedFramework || selectedDataSource;
|
||||
|
||||
if (currentEntity?.internalRedirect && currentEntity?.link) {
|
||||
navigateToPage(currentEntity.link, history.push, e);
|
||||
history.push(currentEntity.link);
|
||||
} else {
|
||||
handleUpdateCurrentStep(2);
|
||||
}
|
||||
@@ -1054,7 +1048,7 @@ function OnboardingAddDataSource(): JSX.Element {
|
||||
<Button
|
||||
type="primary"
|
||||
shape="round"
|
||||
onClick={(e): void => {
|
||||
onClick={(): void => {
|
||||
logEvent(
|
||||
`${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.BASE}: ${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.CONTINUE_BUTTON_CLICKED}`,
|
||||
{
|
||||
@@ -1066,7 +1060,7 @@ function OnboardingAddDataSource(): JSX.Element {
|
||||
);
|
||||
|
||||
handleFilterByCategory('All');
|
||||
handleUpdateCurrentStep(3, e);
|
||||
handleUpdateCurrentStep(3);
|
||||
}}
|
||||
>
|
||||
Continue
|
||||
|
||||
@@ -65,7 +65,6 @@ import AppReducer from 'types/reducer/app';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
import { checkVersionState } from 'utils/app';
|
||||
import { showErrorNotification } from 'utils/error';
|
||||
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
|
||||
|
||||
import { useCmdK } from '../../providers/cmdKProvider';
|
||||
import { routeConfig } from './config';
|
||||
@@ -307,6 +306,8 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
icon: <Cog size={16} />,
|
||||
};
|
||||
|
||||
const isCtrlMetaKey = (e: MouseEvent): boolean => e.ctrlKey || e.metaKey;
|
||||
|
||||
const isLatestVersion = checkVersionState(currentVersion, latestVersion);
|
||||
|
||||
const [
|
||||
@@ -435,6 +436,10 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
|
||||
const isWorkspaceBlocked = trialInfo?.workSpaceBlock || false;
|
||||
|
||||
const openInNewTab = (path: string): void => {
|
||||
window.open(path, '_blank');
|
||||
};
|
||||
|
||||
const onClickGetStarted = (event: MouseEvent): void => {
|
||||
logEvent('Sidebar: Menu clicked', {
|
||||
menuRoute: '/get-started',
|
||||
@@ -445,7 +450,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
? ROUTES.GET_STARTED_WITH_CLOUD
|
||||
: ROUTES.GET_STARTED;
|
||||
|
||||
if (isModifierKeyPressed(event)) {
|
||||
if (isCtrlMetaKey(event)) {
|
||||
openInNewTab(onboaringRoute);
|
||||
} else {
|
||||
history.push(onboaringRoute);
|
||||
@@ -460,7 +465,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
const queryString = getQueryString(availableParams || [], params);
|
||||
|
||||
if (pathname !== key) {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
if (event && isCtrlMetaKey(event)) {
|
||||
openInNewTab(`${key}?${queryString.join('&')}`);
|
||||
} else {
|
||||
history.push(`${key}?${queryString.join('&')}`, {
|
||||
@@ -659,7 +664,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
|
||||
const handleMenuItemClick = (event: MouseEvent, item: SidebarItem): void => {
|
||||
if (item.key === ROUTES.SETTINGS) {
|
||||
if (isModifierKeyPressed(event)) {
|
||||
if (isCtrlMetaKey(event)) {
|
||||
openInNewTab(settingsRoute);
|
||||
} else {
|
||||
history.push(settingsRoute);
|
||||
@@ -837,7 +842,6 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
}
|
||||
};
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
const handleHelpSupportMenuItemClick = (info: SidebarItem): void => {
|
||||
const item = helpSupportDropdownMenuItems.find(
|
||||
(item) => !('type' in item) && item.key === info.key,
|
||||
@@ -847,8 +851,6 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
window.open(item.url, '_blank');
|
||||
}
|
||||
|
||||
const event = (info as SidebarItem & { domEvent?: MouseEvent }).domEvent;
|
||||
|
||||
if (item && !('type' in item)) {
|
||||
logEvent('Help Popover: Item clicked', {
|
||||
menuRoute: item.key,
|
||||
@@ -857,18 +859,10 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
|
||||
switch (item.key) {
|
||||
case ROUTES.SHORTCUTS:
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
openInNewTab(ROUTES.SHORTCUTS);
|
||||
} else {
|
||||
history.push(ROUTES.SHORTCUTS);
|
||||
}
|
||||
history.push(ROUTES.SHORTCUTS);
|
||||
break;
|
||||
case 'invite-collaborators':
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
openInNewTab(`${ROUTES.ORG_SETTINGS}#invite-team-members`);
|
||||
} else {
|
||||
history.push(`${ROUTES.ORG_SETTINGS}#invite-team-members`);
|
||||
}
|
||||
history.push(`${ROUTES.ORG_SETTINGS}#invite-team-members`);
|
||||
break;
|
||||
case 'chat-support':
|
||||
if (window.pylon) {
|
||||
@@ -887,7 +881,6 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
}
|
||||
};
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
const handleSettingsMenuItemClick = (info: SidebarItem): void => {
|
||||
const item = userSettingsDropdownMenuItems.find(
|
||||
(item) => item?.key === info.key,
|
||||
@@ -905,30 +898,15 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
menuRoute: item?.key,
|
||||
menuLabel,
|
||||
});
|
||||
|
||||
const event = (info as SidebarItem & { domEvent?: MouseEvent }).domEvent;
|
||||
|
||||
switch (info.key) {
|
||||
case 'account':
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
openInNewTab(ROUTES.MY_SETTINGS);
|
||||
} else {
|
||||
history.push(ROUTES.MY_SETTINGS);
|
||||
}
|
||||
history.push(ROUTES.MY_SETTINGS);
|
||||
break;
|
||||
case 'workspace':
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
openInNewTab(ROUTES.SETTINGS);
|
||||
} else {
|
||||
history.push(ROUTES.SETTINGS);
|
||||
}
|
||||
history.push(ROUTES.SETTINGS);
|
||||
break;
|
||||
case 'license':
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
openInNewTab(ROUTES.LIST_LICENSES);
|
||||
} else {
|
||||
history.push(ROUTES.LIST_LICENSES);
|
||||
}
|
||||
history.push(ROUTES.LIST_LICENSES);
|
||||
break;
|
||||
case 'logout':
|
||||
Logout();
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
/**
|
||||
* Tests for useSafeNavigate's mock contract.
|
||||
*
|
||||
* The real useSafeNavigate hook is globally replaced by a mock via
|
||||
* jest.config.ts moduleNameMapper, so we cannot test the real
|
||||
* implementation here. Instead we verify:
|
||||
*
|
||||
* 1. The mock accepts the new `event` and `newTab` options without
|
||||
* type errors — ensuring component tests that pass these options
|
||||
* won't break.
|
||||
* 2. The shouldOpenNewTab decision logic (extracted inline below)
|
||||
* matches the real hook's behaviour.
|
||||
*/
|
||||
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import { isModifierKeyPressed } from 'utils/navigation';
|
||||
|
||||
/**
|
||||
* Mirrors the shouldOpenNewTab logic from the real useSafeNavigate hook.
|
||||
* Kept in sync manually — any drift will be caught by integration tests.
|
||||
*/
|
||||
interface NavigateOptions {
|
||||
newTab?: boolean;
|
||||
event?: MouseEvent | React.MouseEvent;
|
||||
}
|
||||
|
||||
const shouldOpenNewTab = (options?: NavigateOptions): boolean =>
|
||||
Boolean(
|
||||
options?.newTab || (options?.event && isModifierKeyPressed(options.event)),
|
||||
);
|
||||
|
||||
describe('useSafeNavigate mock contract', () => {
|
||||
it('mock returns a safeNavigate function', () => {
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
expect(typeof safeNavigate).toBe('function');
|
||||
});
|
||||
|
||||
it('safeNavigate accepts string path with event option', () => {
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
const event = { metaKey: true, ctrlKey: false } as MouseEvent;
|
||||
|
||||
expect(() => {
|
||||
safeNavigate('/alerts', { event });
|
||||
}).not.toThrow();
|
||||
|
||||
expect(safeNavigate).toHaveBeenCalledWith('/alerts', { event });
|
||||
});
|
||||
|
||||
it('safeNavigate accepts string path with newTab option', () => {
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
|
||||
expect(() => {
|
||||
safeNavigate('/dashboard', { newTab: true });
|
||||
}).not.toThrow();
|
||||
|
||||
expect(safeNavigate).toHaveBeenCalledWith('/dashboard', { newTab: true });
|
||||
});
|
||||
|
||||
it('safeNavigate accepts SafeNavigateParams with event option', () => {
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
const event = { metaKey: false, ctrlKey: true } as MouseEvent;
|
||||
|
||||
expect(() => {
|
||||
safeNavigate({ pathname: '/settings', search: '?tab=general' }, { event });
|
||||
}).not.toThrow();
|
||||
|
||||
expect(safeNavigate).toHaveBeenCalledWith(
|
||||
{ pathname: '/settings', search: '?tab=general' },
|
||||
{ event },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('shouldOpenNewTab decision logic', () => {
|
||||
it('returns true when newTab is true', () => {
|
||||
expect(shouldOpenNewTab({ newTab: true })).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true when event has metaKey pressed', () => {
|
||||
const event = { metaKey: true, ctrlKey: false } as MouseEvent;
|
||||
expect(shouldOpenNewTab({ event })).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true when event has ctrlKey pressed', () => {
|
||||
const event = { metaKey: false, ctrlKey: true } as MouseEvent;
|
||||
expect(shouldOpenNewTab({ event })).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when event has no modifier keys', () => {
|
||||
const event = { metaKey: false, ctrlKey: false } as MouseEvent;
|
||||
expect(shouldOpenNewTab({ event })).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when no options provided', () => {
|
||||
expect(shouldOpenNewTab()).toBe(false);
|
||||
expect(shouldOpenNewTab(undefined)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when options provided without event or newTab', () => {
|
||||
expect(shouldOpenNewTab({})).toBe(false);
|
||||
});
|
||||
|
||||
it('newTab takes precedence even without event', () => {
|
||||
expect(shouldOpenNewTab({ newTab: true })).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,13 +1,11 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom-v5-compat';
|
||||
import { cloneDeep, isEqual } from 'lodash-es';
|
||||
import { isModifierKeyPressed } from 'utils/navigation';
|
||||
|
||||
interface NavigateOptions {
|
||||
replace?: boolean;
|
||||
state?: any;
|
||||
newTab?: boolean;
|
||||
event?: MouseEvent | React.MouseEvent;
|
||||
}
|
||||
|
||||
interface SafeNavigateParams {
|
||||
@@ -107,7 +105,6 @@ export const useSafeNavigate = (
|
||||
const location = useLocation();
|
||||
|
||||
const safeNavigate = useCallback(
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
(to: string | SafeNavigateParams, options?: NavigateOptions) => {
|
||||
const currentUrl = new URL(
|
||||
`${location.pathname}${location.search}`,
|
||||
@@ -125,10 +122,8 @@ export const useSafeNavigate = (
|
||||
);
|
||||
}
|
||||
|
||||
const shouldOpenNewTab =
|
||||
options?.newTab || (options?.event && isModifierKeyPressed(options.event));
|
||||
|
||||
if (shouldOpenNewTab) {
|
||||
// If newTab is true, open in new tab and return early
|
||||
if (options?.newTab) {
|
||||
const targetPath =
|
||||
typeof to === 'string'
|
||||
? to
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { Breadcrumb, Button, Divider } from 'antd';
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
NEW_ALERT_SCHEMA_VERSION,
|
||||
PostableAlertRuleV2,
|
||||
} from 'types/api/alerts/alertTypesV2';
|
||||
import { navigateToPage } from 'utils/navigation';
|
||||
|
||||
import AlertHeader from './AlertHeader/AlertHeader';
|
||||
import AlertNotFound from './AlertNotFound';
|
||||
@@ -59,11 +58,11 @@ function BreadCrumbItem({
|
||||
if (isLast) {
|
||||
return <div className="breadcrumb-item breadcrumb-item--last">{title}</div>;
|
||||
}
|
||||
const handleNavigate = (e: React.MouseEvent): void => {
|
||||
const handleNavigate = (): void => {
|
||||
if (!route) {
|
||||
return;
|
||||
}
|
||||
navigateToPage(ROUTES.LIST_ALL_ALERT, history.push, e);
|
||||
history.push(ROUTES.LIST_ALL_ALERT);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from 'react';
|
||||
import { Button, Typography } from 'antd';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
@@ -16,8 +15,8 @@ function AlertNotFound({ isTestAlert }: AlertNotFoundProps): JSX.Element {
|
||||
const { isCloudUser: isCloudUserVal } = useGetTenantLicense();
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
|
||||
const checkAllRulesHandler = (e?: React.MouseEvent): void => {
|
||||
safeNavigate(ROUTES.LIST_ALL_ALERT, { event: e });
|
||||
const checkAllRulesHandler = (): void => {
|
||||
safeNavigate(ROUTES.LIST_ALL_ALERT);
|
||||
};
|
||||
|
||||
const contactSupportHandler = (): void => {
|
||||
|
||||
@@ -69,10 +69,7 @@ describe('AlertNotFound', () => {
|
||||
const user = userEvent.setup();
|
||||
render(<AlertNotFound isTestAlert={false} />);
|
||||
await user.click(screen.getByText('Check all rules'));
|
||||
expect(mockSafeNavigate).toHaveBeenCalledWith(
|
||||
ROUTES.LIST_ALL_ALERT,
|
||||
expect.objectContaining({ event: expect.any(Object) }),
|
||||
);
|
||||
expect(mockSafeNavigate).toHaveBeenCalledWith(ROUTES.LIST_ALL_ALERT);
|
||||
});
|
||||
|
||||
it('should navigate to the correct support page for cloud users when button is clicked', async () => {
|
||||
|
||||
@@ -18,7 +18,7 @@ function AlertTypeSelectionPage(): JSX.Element {
|
||||
}, []);
|
||||
|
||||
const handleSelectType = useCallback(
|
||||
(type: AlertTypes, event?: React.MouseEvent): void => {
|
||||
(type: AlertTypes): void => {
|
||||
// For anamoly based alert, we need to set the ruleType to anomaly_rule
|
||||
// and alertType to metrics_based_alert
|
||||
if (type === AlertTypes.ANOMALY_BASED_ALERT) {
|
||||
@@ -41,7 +41,7 @@ function AlertTypeSelectionPage(): JSX.Element {
|
||||
queryParams.set(QueryParams.showClassicCreateAlertsPage, 'true');
|
||||
}
|
||||
|
||||
safeNavigate(`${ROUTES.ALERTS_NEW}?${queryParams.toString()}`, { event });
|
||||
safeNavigate(`${ROUTES.ALERTS_NEW}?${queryParams.toString()}`);
|
||||
},
|
||||
[queryParams, safeNavigate],
|
||||
);
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { Button, Flex, Typography } from 'antd';
|
||||
import ROUTES from 'constants/routes';
|
||||
import history from 'lib/history';
|
||||
import { ArrowRight } from 'lucide-react';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { navigateToPage } from 'utils/navigation';
|
||||
import { routePermission } from 'utils/permission';
|
||||
|
||||
import './Integrations.styles.scss';
|
||||
|
||||
function Header(): JSX.Element {
|
||||
const history = useHistory();
|
||||
const { user } = useAppContext();
|
||||
|
||||
const isGetStartedWithCloudAllowed = routePermission.GET_STARTED_WITH_CLOUD.includes(
|
||||
@@ -30,9 +30,7 @@ function Header(): JSX.Element {
|
||||
<Button
|
||||
className="periscope-btn primary view-data-sources-btn"
|
||||
type="primary"
|
||||
onClick={(e): void =>
|
||||
navigateToPage(ROUTES.GET_STARTED_WITH_CLOUD, history.push, e)
|
||||
}
|
||||
onClick={(): void => history.push(ROUTES.GET_STARTED_WITH_CLOUD)}
|
||||
>
|
||||
<span>View 150+ Data Sources</span>
|
||||
<ArrowRight size={14} />
|
||||
|
||||
@@ -7,7 +7,6 @@ import { QueryParams } from 'constants/query';
|
||||
import ROUTES from 'constants/routes';
|
||||
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
|
||||
|
||||
import {
|
||||
MessagingQueuesViewType,
|
||||
@@ -65,14 +64,8 @@ function MQDetailPage(): JSX.Element {
|
||||
selectedView !== MessagingQueuesViewType.dropRate.value &&
|
||||
selectedView !== MessagingQueuesViewType.metricPage.value;
|
||||
|
||||
const handleBackClick = (
|
||||
event?: React.MouseEvent | React.KeyboardEvent,
|
||||
): void => {
|
||||
if (event && isModifierKeyPressed(event as React.MouseEvent)) {
|
||||
openInNewTab(ROUTES.MESSAGING_QUEUES_KAFKA);
|
||||
} else {
|
||||
history.push(ROUTES.MESSAGING_QUEUES_KAFKA);
|
||||
}
|
||||
const handleBackClick = (): void => {
|
||||
history.push(ROUTES.MESSAGING_QUEUES_KAFKA);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -84,7 +77,7 @@ function MQDetailPage(): JSX.Element {
|
||||
className="message-queue-text"
|
||||
onKeyDown={(e): void => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
handleBackClick(e);
|
||||
handleBackClick();
|
||||
}
|
||||
}}
|
||||
role="button"
|
||||
|
||||
@@ -30,7 +30,6 @@ import {
|
||||
setConfigDetail,
|
||||
} from 'pages/MessagingQueues/MessagingQueuesUtils';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
|
||||
import { formatNumericValue } from 'utils/numericUtils';
|
||||
|
||||
import { getTableDataForProducerLatencyOverview } from './MQTableUtils';
|
||||
@@ -81,12 +80,7 @@ export function getColumns(
|
||||
onClick={(e): void => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const path = `/services/${encodeURIComponent(text)}`;
|
||||
if (isModifierKeyPressed(e)) {
|
||||
openInNewTab(path);
|
||||
} else {
|
||||
history.push(path);
|
||||
}
|
||||
history.push(`/services/${encodeURIComponent(text)}`);
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
|
||||
@@ -10,7 +10,6 @@ import { QueryParams } from 'constants/query';
|
||||
import ROUTES from 'constants/routes';
|
||||
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
|
||||
|
||||
import {
|
||||
KAFKA_SETUP_DOC_LINK,
|
||||
@@ -24,40 +23,26 @@ function MessagingQueues(): JSX.Element {
|
||||
const history = useHistory();
|
||||
const { t } = useTranslation('messagingQueuesKafkaOverview');
|
||||
|
||||
const redirectToDetailsPage = (
|
||||
callerView?: string,
|
||||
event?: React.MouseEvent,
|
||||
): void => {
|
||||
const redirectToDetailsPage = (callerView?: string): void => {
|
||||
logEvent('Messaging Queues: View details clicked', {
|
||||
page: 'Messaging Queues Overview',
|
||||
source: callerView,
|
||||
});
|
||||
|
||||
const path = `${ROUTES.MESSAGING_QUEUES_KAFKA_DETAIL}?${QueryParams.mqServiceView}=${callerView}`;
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
openInNewTab(path);
|
||||
} else {
|
||||
history.push(path);
|
||||
}
|
||||
history.push(
|
||||
`${ROUTES.MESSAGING_QUEUES_KAFKA_DETAIL}?${QueryParams.mqServiceView}=${callerView}`,
|
||||
);
|
||||
};
|
||||
|
||||
const { isCloudUser: isCloudUserVal } = useGetTenantLicense();
|
||||
|
||||
const getStartedRedirect = (
|
||||
link: string,
|
||||
sourceCard: string,
|
||||
event?: React.MouseEvent,
|
||||
): void => {
|
||||
const getStartedRedirect = (link: string, sourceCard: string): void => {
|
||||
logEvent('Messaging Queues: Get started clicked', {
|
||||
source: sourceCard,
|
||||
link: isCloudUserVal ? link : KAFKA_SETUP_DOC_LINK,
|
||||
});
|
||||
if (isCloudUserVal) {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
openInNewTab(link);
|
||||
} else {
|
||||
history.push(link);
|
||||
}
|
||||
history.push(link);
|
||||
} else {
|
||||
window.open(KAFKA_SETUP_DOC_LINK, '_blank');
|
||||
}
|
||||
@@ -94,11 +79,10 @@ function MessagingQueues(): JSX.Element {
|
||||
<div className="button-grp">
|
||||
<Button
|
||||
type="default"
|
||||
onClick={(e): void =>
|
||||
onClick={(): void =>
|
||||
getStartedRedirect(
|
||||
`${ROUTES.GET_STARTED_APPLICATION_MONITORING}?${QueryParams.getStartedSource}=kafka&${QueryParams.getStartedSourceService}=${MessagingQueueHealthCheckService.Consumers}`,
|
||||
'Configure Consumer',
|
||||
e,
|
||||
)
|
||||
}
|
||||
>
|
||||
@@ -114,11 +98,10 @@ function MessagingQueues(): JSX.Element {
|
||||
<div className="button-grp">
|
||||
<Button
|
||||
type="default"
|
||||
onClick={(e): void =>
|
||||
onClick={(): void =>
|
||||
getStartedRedirect(
|
||||
`${ROUTES.GET_STARTED_APPLICATION_MONITORING}?${QueryParams.getStartedSource}=kafka&${QueryParams.getStartedSourceService}=${MessagingQueueHealthCheckService.Producers}`,
|
||||
'Configure Producer',
|
||||
e,
|
||||
)
|
||||
}
|
||||
>
|
||||
@@ -134,11 +117,10 @@ function MessagingQueues(): JSX.Element {
|
||||
<div className="button-grp">
|
||||
<Button
|
||||
type="default"
|
||||
onClick={(e): void =>
|
||||
onClick={(): void =>
|
||||
getStartedRedirect(
|
||||
`${ROUTES.GET_STARTED_INFRASTRUCTURE_MONITORING}?${QueryParams.getStartedSource}=kafka&${QueryParams.getStartedSourceService}=${MessagingQueueHealthCheckService.Kafka}`,
|
||||
'Monitor kafka',
|
||||
e,
|
||||
)
|
||||
}
|
||||
>
|
||||
@@ -161,8 +143,8 @@ function MessagingQueues(): JSX.Element {
|
||||
<div className="button-grp">
|
||||
<Button
|
||||
type="default"
|
||||
onClick={(e): void =>
|
||||
redirectToDetailsPage(MessagingQueuesViewType.consumerLag.value, e)
|
||||
onClick={(): void =>
|
||||
redirectToDetailsPage(MessagingQueuesViewType.consumerLag.value)
|
||||
}
|
||||
>
|
||||
{t('summarySection.viewDetailsButton')}
|
||||
@@ -179,8 +161,8 @@ function MessagingQueues(): JSX.Element {
|
||||
<div className="button-grp">
|
||||
<Button
|
||||
type="default"
|
||||
onClick={(e): void =>
|
||||
redirectToDetailsPage(MessagingQueuesViewType.producerLatency.value, e)
|
||||
onClick={(): void =>
|
||||
redirectToDetailsPage(MessagingQueuesViewType.producerLatency.value)
|
||||
}
|
||||
>
|
||||
{t('summarySection.viewDetailsButton')}
|
||||
@@ -197,11 +179,8 @@ function MessagingQueues(): JSX.Element {
|
||||
<div className="button-grp">
|
||||
<Button
|
||||
type="default"
|
||||
onClick={(e): void =>
|
||||
redirectToDetailsPage(
|
||||
MessagingQueuesViewType.partitionLatency.value,
|
||||
e,
|
||||
)
|
||||
onClick={(): void =>
|
||||
redirectToDetailsPage(MessagingQueuesViewType.partitionLatency.value)
|
||||
}
|
||||
>
|
||||
{t('summarySection.viewDetailsButton')}
|
||||
@@ -218,8 +197,8 @@ function MessagingQueues(): JSX.Element {
|
||||
<div className="button-grp">
|
||||
<Button
|
||||
type="default"
|
||||
onClick={(e): void =>
|
||||
redirectToDetailsPage(MessagingQueuesViewType.dropRate.value, e)
|
||||
onClick={(): void =>
|
||||
redirectToDetailsPage(MessagingQueuesViewType.dropRate.value)
|
||||
}
|
||||
>
|
||||
{t('summarySection.viewDetailsButton')}
|
||||
@@ -236,8 +215,8 @@ function MessagingQueues(): JSX.Element {
|
||||
<div className="button-grp">
|
||||
<Button
|
||||
type="default"
|
||||
onClick={(e): void =>
|
||||
redirectToDetailsPage(MessagingQueuesViewType.metricPage.value, e)
|
||||
onClick={(): void =>
|
||||
redirectToDetailsPage(MessagingQueuesViewType.metricPage.value)
|
||||
}
|
||||
>
|
||||
{t('summarySection.viewDetailsButton')}
|
||||
|
||||
@@ -16,7 +16,6 @@ import history from 'lib/history';
|
||||
import { Cog } from 'lucide-react';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
|
||||
|
||||
import { getRoutes } from './utils';
|
||||
|
||||
@@ -185,6 +184,12 @@ function SettingsPage(): JSX.Element {
|
||||
],
|
||||
);
|
||||
|
||||
const isCtrlMetaKey = (e: MouseEvent): boolean => e.ctrlKey || e.metaKey;
|
||||
|
||||
const openInNewTab = (path: string): void => {
|
||||
window.open(path, '_blank');
|
||||
};
|
||||
|
||||
const onClickHandler = useCallback(
|
||||
(key: string, event: MouseEvent | null) => {
|
||||
const params = new URLSearchParams(search);
|
||||
@@ -193,7 +198,7 @@ function SettingsPage(): JSX.Element {
|
||||
const queryString = getQueryString(availableParams || [], params);
|
||||
|
||||
if (pathname !== key) {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
if (event && isCtrlMetaKey(event)) {
|
||||
openInNewTab(`${key}?${queryString.join('&')}`);
|
||||
} else {
|
||||
history.push(`${key}?${queryString.join('&')}`, {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable react/no-unescaped-entities */
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useMutation } from 'react-query';
|
||||
import type { TabsProps } from 'antd';
|
||||
@@ -27,7 +27,6 @@ import { CircleArrowRight } from 'lucide-react';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import APIError from 'types/api/error';
|
||||
import { LicensePlatform } from 'types/api/licensesV3/getActive';
|
||||
import { navigateToPage } from 'utils/navigation';
|
||||
import { getFormattedDate } from 'utils/timeUtils';
|
||||
|
||||
import CustomerStoryCard from './CustomerStoryCard';
|
||||
@@ -134,10 +133,10 @@ export default function WorkspaceBlocked(): JSX.Element {
|
||||
});
|
||||
};
|
||||
|
||||
const handleViewBilling = (e?: React.MouseEvent): void => {
|
||||
const handleViewBilling = (): void => {
|
||||
logEvent('Workspace Blocked: User Clicked View Billing', {});
|
||||
|
||||
navigateToPage(ROUTES.BILLING, history.push, e);
|
||||
history.push(ROUTES.BILLING);
|
||||
};
|
||||
|
||||
const renderCustomerStories = (
|
||||
@@ -297,7 +296,7 @@ export default function WorkspaceBlocked(): JSX.Element {
|
||||
type="link"
|
||||
size="small"
|
||||
role="button"
|
||||
onClick={(e): void => handleViewBilling(e)}
|
||||
onClick={handleViewBilling}
|
||||
>
|
||||
View Billing
|
||||
</Button>
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
import {
|
||||
isModifierKeyPressed,
|
||||
navigateToPage,
|
||||
openInNewTab,
|
||||
} from '../navigation';
|
||||
|
||||
describe('navigation utilities', () => {
|
||||
const originalWindowOpen = window.open;
|
||||
|
||||
beforeEach(() => {
|
||||
window.open = jest.fn();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.open = originalWindowOpen;
|
||||
});
|
||||
|
||||
describe('isModifierKeyPressed', () => {
|
||||
const createMouseEvent = (overrides: Partial<MouseEvent> = {}): MouseEvent =>
|
||||
({
|
||||
metaKey: false,
|
||||
ctrlKey: false,
|
||||
...overrides,
|
||||
} as MouseEvent);
|
||||
|
||||
it('returns true when metaKey is pressed (Cmd on Mac)', () => {
|
||||
const event = createMouseEvent({ metaKey: true });
|
||||
expect(isModifierKeyPressed(event)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true when ctrlKey is pressed (Ctrl on Windows/Linux)', () => {
|
||||
const event = createMouseEvent({ ctrlKey: true });
|
||||
expect(isModifierKeyPressed(event)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true when both metaKey and ctrlKey are pressed', () => {
|
||||
const event = createMouseEvent({ metaKey: true, ctrlKey: true });
|
||||
expect(isModifierKeyPressed(event)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when neither modifier key is pressed', () => {
|
||||
const event = createMouseEvent();
|
||||
expect(isModifierKeyPressed(event)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when only shiftKey or altKey are pressed', () => {
|
||||
const event = createMouseEvent({
|
||||
shiftKey: true,
|
||||
altKey: true,
|
||||
} as Partial<MouseEvent>);
|
||||
expect(isModifierKeyPressed(event)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('openInNewTab', () => {
|
||||
it('calls window.open with the given path and _blank target', () => {
|
||||
openInNewTab('/dashboard');
|
||||
expect(window.open).toHaveBeenCalledWith('/dashboard', '_blank');
|
||||
});
|
||||
|
||||
it('handles full URLs', () => {
|
||||
openInNewTab('https://example.com/page');
|
||||
expect(window.open).toHaveBeenCalledWith(
|
||||
'https://example.com/page',
|
||||
'_blank',
|
||||
);
|
||||
});
|
||||
|
||||
it('handles paths with query strings', () => {
|
||||
openInNewTab('/alerts?tab=AlertRules&relativeTime=30m');
|
||||
expect(window.open).toHaveBeenCalledWith(
|
||||
'/alerts?tab=AlertRules&relativeTime=30m',
|
||||
'_blank',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('navigateToPage', () => {
|
||||
const mockNavigate = jest.fn() as jest.MockedFunction<(path: string) => void>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockNavigate.mockClear();
|
||||
});
|
||||
|
||||
it('opens new tab when metaKey is pressed', () => {
|
||||
const event = { metaKey: true, ctrlKey: false } as MouseEvent;
|
||||
navigateToPage('/services', mockNavigate, event);
|
||||
|
||||
expect(window.open).toHaveBeenCalledWith('/services', '_blank');
|
||||
expect(mockNavigate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('opens new tab when ctrlKey is pressed', () => {
|
||||
const event = { metaKey: false, ctrlKey: true } as MouseEvent;
|
||||
navigateToPage('/services', mockNavigate, event);
|
||||
|
||||
expect(window.open).toHaveBeenCalledWith('/services', '_blank');
|
||||
expect(mockNavigate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls navigate callback when no modifier key is pressed', () => {
|
||||
const event = { metaKey: false, ctrlKey: false } as MouseEvent;
|
||||
navigateToPage('/services', mockNavigate, event);
|
||||
|
||||
expect(mockNavigate).toHaveBeenCalledWith('/services');
|
||||
expect(window.open).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls navigate callback when no event is provided', () => {
|
||||
navigateToPage('/services', mockNavigate);
|
||||
|
||||
expect(mockNavigate).toHaveBeenCalledWith('/services');
|
||||
expect(window.open).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls navigate callback when event is undefined', () => {
|
||||
navigateToPage('/dashboard', mockNavigate, undefined);
|
||||
|
||||
expect(mockNavigate).toHaveBeenCalledWith('/dashboard');
|
||||
expect(window.open).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,37 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
* Returns true if the user is holding Cmd (Mac) or Ctrl (Windows/Linux)
|
||||
* during a click event — the universal "open in new tab" modifier.
|
||||
*/
|
||||
export const isModifierKeyPressed = (
|
||||
event: MouseEvent | React.MouseEvent,
|
||||
): boolean => event.metaKey || event.ctrlKey;
|
||||
|
||||
/**
|
||||
* Opens the given path in a new browser tab.
|
||||
*/
|
||||
export const openInNewTab = (path: string): void => {
|
||||
window.open(path, '_blank');
|
||||
};
|
||||
|
||||
/**
|
||||
* Navigates to a path, respecting modifier keys. If Cmd/Ctrl is held,
|
||||
* the path is opened in a new tab. Otherwise, the provided `navigate`
|
||||
* callback is invoked for SPA navigation.
|
||||
*
|
||||
* @param path - The target URL path
|
||||
* @param navigate - SPA navigation callback (e.g. history.push, safeNavigate)
|
||||
* @param event - Optional mouse event to check for modifier keys
|
||||
*/
|
||||
export const navigateToPage = (
|
||||
path: string,
|
||||
navigate: (path: string) => void,
|
||||
event?: MouseEvent | React.MouseEvent,
|
||||
): void => {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
openInNewTab(path);
|
||||
} else {
|
||||
navigate(path);
|
||||
}
|
||||
};
|
||||
@@ -7,9 +7,9 @@ import (
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/query-service/constants"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/model"
|
||||
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
|
||||
"github.com/SigNoz/signoz/pkg/types/pipelinetypes"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
@@ -96,10 +96,10 @@ var buildProcessorTestData = []struct {
|
||||
|
||||
func TestBuildLogParsingProcessors(t *testing.T) {
|
||||
for _, test := range buildProcessorTestData {
|
||||
Convey(test.Name, t, func() {
|
||||
t.Run(test.Name, func(t *testing.T) {
|
||||
err := updateProcessorConfigsInCollectorConf(test.agentConf, test.pipelineProcessor)
|
||||
So(err, ShouldBeNil)
|
||||
So(test.agentConf, ShouldResemble, test.outputConf)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, test.outputConf, test.agentConf)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -202,11 +202,11 @@ var BuildLogsPipelineTestData = []struct {
|
||||
|
||||
func TestBuildLogsPipeline(t *testing.T) {
|
||||
for _, test := range BuildLogsPipelineTestData {
|
||||
Convey(test.Name, t, func() {
|
||||
t.Run(test.Name, func(t *testing.T) {
|
||||
v, err := buildCollectorPipelineProcessorsList(test.currentPipeline, test.logsPipeline)
|
||||
So(err, ShouldBeNil)
|
||||
assert.NoError(t, err)
|
||||
fmt.Println(test.Name, "\n", test.currentPipeline, "\n", v, "\n", test.expectedPipeline)
|
||||
So(v, ShouldResemble, test.expectedPipeline)
|
||||
assert.Equal(t, test.expectedPipeline, v)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -239,19 +239,8 @@ func TestPipelineAliasCollisionsDontResultInDuplicateCollectorProcessors(t *test
|
||||
Alias: alias,
|
||||
Enabled: true,
|
||||
},
|
||||
Filter: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "method",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
},
|
||||
Operator: "=",
|
||||
Value: "GET",
|
||||
},
|
||||
},
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "attribute.method = 'GET'",
|
||||
},
|
||||
Config: []pipelinetypes.PipelineOperator{
|
||||
{
|
||||
@@ -310,19 +299,8 @@ func TestPipelineRouterWorksEvenIfFirstOpIsDisabled(t *testing.T) {
|
||||
Alias: "pipeline1",
|
||||
Enabled: true,
|
||||
},
|
||||
Filter: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "method",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
},
|
||||
Operator: "=",
|
||||
Value: "GET",
|
||||
},
|
||||
},
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "attribute.method = 'GET'",
|
||||
},
|
||||
Config: []pipelinetypes.PipelineOperator{
|
||||
{
|
||||
@@ -383,19 +361,8 @@ func TestPipeCharInAliasDoesntBreakCollectorConfig(t *testing.T) {
|
||||
Alias: "test|pipeline",
|
||||
Enabled: true,
|
||||
},
|
||||
Filter: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "method",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
},
|
||||
Operator: "=",
|
||||
Value: "GET",
|
||||
},
|
||||
},
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "attribute.method = 'GET'",
|
||||
},
|
||||
Config: []pipelinetypes.PipelineOperator{
|
||||
{
|
||||
|
||||
@@ -11,13 +11,13 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/query-service/agentConf"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/constants"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/model"
|
||||
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/utils"
|
||||
"github.com/SigNoz/signoz/pkg/querybuilder"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/opamptypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/pipelinetypes"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/google/uuid"
|
||||
|
||||
@@ -140,15 +140,8 @@ func (ic *LogParsingPipelineController) getDefaultPipelines() ([]pipelinetypes.G
|
||||
Alias: "NormalizeBodyDefault",
|
||||
Enabled: true,
|
||||
},
|
||||
Filter: &v3.FilterSet{
|
||||
Items: []v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "body",
|
||||
},
|
||||
Operator: v3.FilterOperatorExists,
|
||||
},
|
||||
},
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "body EXISTS",
|
||||
},
|
||||
Config: []pipelinetypes.PipelineOperator{
|
||||
{
|
||||
|
||||
@@ -9,14 +9,14 @@ import (
|
||||
|
||||
signozstanzahelper "github.com/SigNoz/signoz-otel-collector/processor/signozlogspipelineprocessor/stanza/operator/helper"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/model"
|
||||
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/utils"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/pipelinetypes"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/google/uuid"
|
||||
"github.com/open-telemetry/opentelemetry-collector-contrib/pkg/stanza/entry"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -203,10 +203,10 @@ var prepareProcessorTestData = []struct {
|
||||
|
||||
func TestPreparePipelineProcessor(t *testing.T) {
|
||||
for _, test := range prepareProcessorTestData {
|
||||
Convey(test.Name, t, func() {
|
||||
t.Run(test.Name, func(t *testing.T) {
|
||||
res, err := getOperators(test.Operators)
|
||||
So(err, ShouldBeNil)
|
||||
So(res, ShouldResemble, test.Output)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, test.Output, res)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -214,19 +214,8 @@ func TestPreparePipelineProcessor(t *testing.T) {
|
||||
func TestNoCollectorErrorsFromProcessorsForMismatchedLogs(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
testPipelineFilter := &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "method",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
},
|
||||
Operator: "=",
|
||||
Value: "GET",
|
||||
},
|
||||
},
|
||||
testPipelineFilter := &qbtypes.Filter{
|
||||
Expression: "attribute.method = 'GET'",
|
||||
}
|
||||
makeTestPipeline := func(config []pipelinetypes.PipelineOperator) pipelinetypes.GettablePipeline {
|
||||
return pipelinetypes.GettablePipeline{
|
||||
@@ -470,19 +459,8 @@ func TestResourceFiltersWork(t *testing.T) {
|
||||
Alias: "pipeline1",
|
||||
Enabled: true,
|
||||
},
|
||||
Filter: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "service",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeResource,
|
||||
},
|
||||
Operator: "=",
|
||||
Value: "nginx",
|
||||
},
|
||||
},
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "resource.service = 'nginx'",
|
||||
},
|
||||
Config: []pipelinetypes.PipelineOperator{
|
||||
{
|
||||
@@ -524,11 +502,11 @@ func TestResourceFiltersWork(t *testing.T) {
|
||||
func TestPipelineFilterWithStringOpsShouldNotSpamWarningsIfAttributeIsMissing(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
for _, operator := range []v3.FilterOperator{
|
||||
v3.FilterOperatorContains,
|
||||
v3.FilterOperatorNotContains,
|
||||
v3.FilterOperatorRegex,
|
||||
v3.FilterOperatorNotRegex,
|
||||
for _, operator := range []string{
|
||||
"CONTAINS",
|
||||
"NOT CONTAINS",
|
||||
"REGEXP",
|
||||
"NOT REGEXP",
|
||||
} {
|
||||
testPipeline := pipelinetypes.GettablePipeline{
|
||||
StoreablePipeline: pipelinetypes.StoreablePipeline{
|
||||
@@ -540,19 +518,8 @@ func TestPipelineFilterWithStringOpsShouldNotSpamWarningsIfAttributeIsMissing(t
|
||||
Alias: "pipeline1",
|
||||
Enabled: true,
|
||||
},
|
||||
Filter: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "service",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeResource,
|
||||
},
|
||||
Operator: operator,
|
||||
Value: "nginx",
|
||||
},
|
||||
},
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: fmt.Sprintf("resource.service %s 'nginx'", operator),
|
||||
},
|
||||
Config: []pipelinetypes.PipelineOperator{
|
||||
{
|
||||
@@ -601,19 +568,8 @@ func TestAttributePathsContainingDollarDoNotBreakCollector(t *testing.T) {
|
||||
Alias: "pipeline1",
|
||||
Enabled: true,
|
||||
},
|
||||
Filter: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "$test",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
},
|
||||
Operator: "=",
|
||||
Value: "test",
|
||||
},
|
||||
},
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "attribute.$test = 'test'",
|
||||
},
|
||||
Config: []pipelinetypes.PipelineOperator{
|
||||
{
|
||||
@@ -664,19 +620,8 @@ func TestMembershipOpInProcessorFieldExpressions(t *testing.T) {
|
||||
Alias: "pipeline1",
|
||||
Enabled: true,
|
||||
},
|
||||
Filter: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "http.method",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
},
|
||||
Operator: "=",
|
||||
Value: "GET",
|
||||
},
|
||||
},
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "attribute.http.method = 'GET'",
|
||||
},
|
||||
Config: []pipelinetypes.PipelineOperator{
|
||||
{
|
||||
@@ -755,7 +700,7 @@ func TestMembershipOpInProcessorFieldExpressions(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestContainsFilterIsCaseInsensitive(t *testing.T) {
|
||||
// The contains and ncontains query builder filters are case insensitive when querying logs.
|
||||
// The CONTAINS and NOT CONTAINS query builder filters are case insensitive when querying logs.
|
||||
// Pipeline filter should also behave in the same way.
|
||||
require := require.New(t)
|
||||
|
||||
@@ -773,18 +718,8 @@ func TestContainsFilterIsCaseInsensitive(t *testing.T) {
|
||||
Alias: "pipeline1",
|
||||
Enabled: true,
|
||||
},
|
||||
Filter: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "body",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeUnspecified,
|
||||
IsColumn: true,
|
||||
},
|
||||
Operator: "contains",
|
||||
Value: "log",
|
||||
}},
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "body CONTAINS 'log'",
|
||||
},
|
||||
Config: []pipelinetypes.PipelineOperator{
|
||||
{
|
||||
@@ -806,18 +741,8 @@ func TestContainsFilterIsCaseInsensitive(t *testing.T) {
|
||||
Alias: "pipeline2",
|
||||
Enabled: true,
|
||||
},
|
||||
Filter: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "body",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeUnspecified,
|
||||
IsColumn: true,
|
||||
},
|
||||
Operator: "ncontains",
|
||||
Value: "ecom",
|
||||
}},
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "body NOT CONTAINS 'ecom'",
|
||||
},
|
||||
Config: []pipelinetypes.PipelineOperator{
|
||||
{
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/query-service/model"
|
||||
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
|
||||
"github.com/SigNoz/signoz/pkg/types/pipelinetypes"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/google/uuid"
|
||||
"github.com/open-telemetry/opentelemetry-collector-contrib/pkg/stanza/entry"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -25,19 +25,8 @@ func TestPipelinePreview(t *testing.T) {
|
||||
Alias: "pipeline1",
|
||||
Enabled: true,
|
||||
},
|
||||
Filter: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "method",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
},
|
||||
Operator: "=",
|
||||
Value: "GET",
|
||||
},
|
||||
},
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "attribute.method = 'GET'",
|
||||
},
|
||||
Config: []pipelinetypes.PipelineOperator{
|
||||
{
|
||||
@@ -58,19 +47,8 @@ func TestPipelinePreview(t *testing.T) {
|
||||
Alias: "pipeline2",
|
||||
Enabled: true,
|
||||
},
|
||||
Filter: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "method",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
},
|
||||
Operator: "=",
|
||||
Value: "GET",
|
||||
},
|
||||
},
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "attribute.method = 'GET'",
|
||||
},
|
||||
Config: []pipelinetypes.PipelineOperator{
|
||||
{
|
||||
@@ -159,19 +137,8 @@ func TestGrokParsingProcessor(t *testing.T) {
|
||||
Alias: "pipeline1",
|
||||
Enabled: true,
|
||||
},
|
||||
Filter: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "method",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
},
|
||||
Operator: "=",
|
||||
Value: "GET",
|
||||
},
|
||||
},
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "attribute.method = 'GET'",
|
||||
},
|
||||
Config: []pipelinetypes.PipelineOperator{
|
||||
{
|
||||
|
||||
@@ -10,8 +10,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/query-service/model"
|
||||
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/utils"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/pipelinetypes"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -30,19 +30,8 @@ func TestRegexProcessor(t *testing.T) {
|
||||
Alias: "pipeline1",
|
||||
Enabled: true,
|
||||
},
|
||||
Filter: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "method",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
},
|
||||
Operator: "=",
|
||||
Value: "GET",
|
||||
},
|
||||
},
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "attribute.method = 'GET'",
|
||||
},
|
||||
Config: []pipelinetypes.PipelineOperator{},
|
||||
},
|
||||
@@ -97,19 +86,8 @@ func TestGrokProcessor(t *testing.T) {
|
||||
Alias: "pipeline1",
|
||||
Enabled: true,
|
||||
},
|
||||
Filter: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "method",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
},
|
||||
Operator: "=",
|
||||
Value: "GET",
|
||||
},
|
||||
},
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "attribute.method = 'GET'",
|
||||
},
|
||||
Config: []pipelinetypes.PipelineOperator{},
|
||||
},
|
||||
@@ -164,19 +142,8 @@ func TestJSONProcessor(t *testing.T) {
|
||||
Alias: "pipeline1",
|
||||
Enabled: true,
|
||||
},
|
||||
Filter: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "method",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
},
|
||||
Operator: "=",
|
||||
Value: "GET",
|
||||
},
|
||||
},
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "attribute.method = 'GET'",
|
||||
},
|
||||
Config: []pipelinetypes.PipelineOperator{},
|
||||
},
|
||||
@@ -230,20 +197,9 @@ func TestTraceParsingProcessor(t *testing.T) {
|
||||
Alias: "pipeline1",
|
||||
Enabled: true,
|
||||
},
|
||||
Filter: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "method",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
},
|
||||
Operator: "=",
|
||||
Value: "GET",
|
||||
},
|
||||
},
|
||||
},
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "attribute.method = 'GET'",
|
||||
},
|
||||
Config: []pipelinetypes.PipelineOperator{},
|
||||
},
|
||||
}
|
||||
@@ -339,19 +295,8 @@ func TestAddProcessor(t *testing.T) {
|
||||
Alias: "pipeline1",
|
||||
Enabled: true,
|
||||
},
|
||||
Filter: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "method",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
},
|
||||
Operator: "=",
|
||||
Value: "GET",
|
||||
},
|
||||
},
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "attribute.method = 'GET'",
|
||||
},
|
||||
Config: []pipelinetypes.PipelineOperator{},
|
||||
},
|
||||
@@ -404,19 +349,8 @@ func TestRemoveProcessor(t *testing.T) {
|
||||
Alias: "pipeline1",
|
||||
Enabled: true,
|
||||
},
|
||||
Filter: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "method",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
},
|
||||
Operator: "=",
|
||||
Value: "GET",
|
||||
},
|
||||
},
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "attribute.method = 'GET'",
|
||||
},
|
||||
Config: []pipelinetypes.PipelineOperator{},
|
||||
},
|
||||
@@ -469,19 +403,8 @@ func TestCopyProcessor(t *testing.T) {
|
||||
Alias: "pipeline1",
|
||||
Enabled: true,
|
||||
},
|
||||
Filter: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "method",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
},
|
||||
Operator: "=",
|
||||
Value: "GET",
|
||||
},
|
||||
},
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "attribute.method = 'GET'",
|
||||
},
|
||||
Config: []pipelinetypes.PipelineOperator{},
|
||||
},
|
||||
@@ -535,19 +458,8 @@ func TestMoveProcessor(t *testing.T) {
|
||||
Alias: "pipeline1",
|
||||
Enabled: true,
|
||||
},
|
||||
Filter: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "method",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
},
|
||||
Operator: "=",
|
||||
Value: "GET",
|
||||
},
|
||||
},
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "attribute.method = 'GET'",
|
||||
},
|
||||
Config: []pipelinetypes.PipelineOperator{},
|
||||
},
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/query-service/model"
|
||||
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
|
||||
"github.com/SigNoz/signoz/pkg/types/pipelinetypes"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -23,19 +23,8 @@ func TestSeverityParsingProcessor(t *testing.T) {
|
||||
Alias: "pipeline1",
|
||||
Enabled: true,
|
||||
},
|
||||
Filter: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "method",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
},
|
||||
Operator: "=",
|
||||
Value: "GET",
|
||||
},
|
||||
},
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "attribute.method = 'GET'",
|
||||
},
|
||||
Config: []pipelinetypes.PipelineOperator{},
|
||||
},
|
||||
@@ -141,19 +130,8 @@ func TestSeverityParsingProcessor(t *testing.T) {
|
||||
func TestNoCollectorErrorsFromSeverityParserForMismatchedLogs(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
testPipelineFilter := &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "method",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
},
|
||||
Operator: "=",
|
||||
Value: "GET",
|
||||
},
|
||||
},
|
||||
testPipelineFilter := &qbtypes.Filter{
|
||||
Expression: "attribute.method = 'GET'",
|
||||
}
|
||||
makeTestPipeline := func(config []pipelinetypes.PipelineOperator) pipelinetypes.GettablePipeline {
|
||||
return pipelinetypes.GettablePipeline{
|
||||
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/query-service/model"
|
||||
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
|
||||
"github.com/SigNoz/signoz/pkg/types/pipelinetypes"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -24,19 +24,8 @@ func TestTimestampParsingProcessor(t *testing.T) {
|
||||
Alias: "pipeline1",
|
||||
Enabled: true,
|
||||
},
|
||||
Filter: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "method",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
},
|
||||
Operator: "=",
|
||||
Value: "GET",
|
||||
},
|
||||
},
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "attribute.method = 'GET'",
|
||||
},
|
||||
Config: []pipelinetypes.PipelineOperator{},
|
||||
},
|
||||
|
||||
@@ -2,11 +2,16 @@ package queryBuilderToExpr
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"maps"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
|
||||
"github.com/SigNoz/signoz/pkg/querybuilder"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrylogs"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
expr "github.com/antonmedv/expr"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
@@ -15,119 +20,233 @@ var (
|
||||
CodeExprCompilationFailed = errors.MustNewCode("expr_compilation_failed")
|
||||
)
|
||||
|
||||
var logOperatorsToExpr = map[v3.FilterOperator]string{
|
||||
v3.FilterOperatorEqual: "==",
|
||||
v3.FilterOperatorNotEqual: "!=",
|
||||
v3.FilterOperatorLessThan: "<",
|
||||
v3.FilterOperatorLessThanOrEq: "<=",
|
||||
v3.FilterOperatorGreaterThan: ">",
|
||||
v3.FilterOperatorGreaterThanOrEq: ">=",
|
||||
v3.FilterOperatorContains: "contains",
|
||||
v3.FilterOperatorNotContains: "not contains",
|
||||
v3.FilterOperatorRegex: "matches",
|
||||
v3.FilterOperatorNotRegex: "not matches",
|
||||
v3.FilterOperatorIn: "in",
|
||||
v3.FilterOperatorNotIn: "not in",
|
||||
v3.FilterOperatorExists: "in",
|
||||
v3.FilterOperatorNotExists: "not in",
|
||||
// we dont support like and nlike as of now.
|
||||
var logOperatorsToExpr = map[qbtypes.FilterOperator]string{
|
||||
qbtypes.FilterOperatorEqual: "==",
|
||||
qbtypes.FilterOperatorNotEqual: "!=",
|
||||
qbtypes.FilterOperatorLessThan: "<",
|
||||
qbtypes.FilterOperatorLessThanOrEq: "<=",
|
||||
qbtypes.FilterOperatorGreaterThan: ">",
|
||||
qbtypes.FilterOperatorGreaterThanOrEq: ">=",
|
||||
qbtypes.FilterOperatorContains: "contains",
|
||||
qbtypes.FilterOperatorNotContains: "not contains",
|
||||
qbtypes.FilterOperatorRegexp: "matches",
|
||||
qbtypes.FilterOperatorNotRegexp: "not matches",
|
||||
qbtypes.FilterOperatorIn: "in",
|
||||
qbtypes.FilterOperatorNotIn: "not in",
|
||||
qbtypes.FilterOperatorExists: "in",
|
||||
qbtypes.FilterOperatorNotExists: "not in",
|
||||
// nlike and like are not supported yet
|
||||
}
|
||||
|
||||
func getName(v v3.AttributeKey) string {
|
||||
if v.Type == v3.AttributeKeyTypeTag {
|
||||
return fmt.Sprintf(`attributes["%s"]`, v.Key)
|
||||
} else if v.Type == v3.AttributeKeyTypeResource {
|
||||
return fmt.Sprintf(`resource["%s"]`, v.Key)
|
||||
func getName(key *telemetrytypes.TelemetryFieldKey) string {
|
||||
if key == nil {
|
||||
return ""
|
||||
}
|
||||
return v.Key
|
||||
}
|
||||
|
||||
func getTypeName(v v3.AttributeKeyType) string {
|
||||
if v == v3.AttributeKeyTypeTag {
|
||||
return "attributes"
|
||||
} else if v == v3.AttributeKeyTypeResource {
|
||||
return "resource"
|
||||
switch key.FieldContext {
|
||||
case telemetrytypes.FieldContextAttribute:
|
||||
return fmt.Sprintf(`attributes["%s"]`, key.Name)
|
||||
case telemetrytypes.FieldContextResource:
|
||||
return fmt.Sprintf(`resource["%s"]`, key.Name)
|
||||
case telemetrytypes.FieldContextBody:
|
||||
return fmt.Sprintf("%s.%s", key.FieldContext.StringValue(), key.Name)
|
||||
default:
|
||||
return key.Name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func Parse(filters *v3.FilterSet) (string, error) {
|
||||
var res []string
|
||||
for _, v := range filters.Items {
|
||||
if _, ok := logOperatorsToExpr[v.Operator]; !ok {
|
||||
return "", errors.NewInvalidInputf(errors.CodeInvalidInput, "operator not supported: %s", v.Operator)
|
||||
}
|
||||
func parseCondition(c qbtypes.FilterCondition) (string, error) {
|
||||
if c.Key == nil {
|
||||
return "", errors.NewInvalidInputf(errors.CodeInvalidInput, "condition has no keys")
|
||||
}
|
||||
|
||||
name := getName(v.Key)
|
||||
if _, ok := logOperatorsToExpr[c.Op]; !ok {
|
||||
return "", errors.NewInvalidInputf(errors.CodeInvalidInput, "operator not supported: %d", c.Op)
|
||||
}
|
||||
|
||||
var filter string
|
||||
value := exprFormattedValue(c.Value)
|
||||
var filter string
|
||||
|
||||
switch v.Operator {
|
||||
// uncomment following lines when new version of expr is used
|
||||
// case v3.FilterOperatorIn, v3.FilterOperatorNotIn:
|
||||
// filter = fmt.Sprintf("%s %s list%s", name, logOperatorsToExpr[v.Operator], exprFormattedValue(v.Value))
|
||||
switch c.Op {
|
||||
case qbtypes.FilterOperatorExists, qbtypes.FilterOperatorNotExists:
|
||||
// EXISTS/NOT EXISTS checks are special:
|
||||
// - For body fields, we check membership in fromJSON(body) using the JSON field name.
|
||||
// - For attribute/resource fields, we check membership in the appropriate map
|
||||
// ("attributes" or "resource") using the logical field name.
|
||||
// - For intrinsic / top‑level fields (no explicit context), we fall back to
|
||||
// equality against nil (see default case below).
|
||||
switch c.Key.FieldContext {
|
||||
case telemetrytypes.FieldContextBody:
|
||||
// if body is a string and is a valid JSON, then check if the key exists in the JSON
|
||||
quoted := exprFormattedValue(c.Key.Name)
|
||||
jsonMembership := fmt.Sprintf(
|
||||
`((type(body) == "string" && isJSON(body)) && %s %s %s)`,
|
||||
quoted, logOperatorsToExpr[c.Op], "fromJSON(body)",
|
||||
)
|
||||
|
||||
case v3.FilterOperatorExists, v3.FilterOperatorNotExists:
|
||||
// accustom log filters like `body.log.message EXISTS` into EXPR language
|
||||
// where User is attempting to check for keys present in JSON log body
|
||||
if strings.HasPrefix(v.Key.Key, "body.") {
|
||||
// if body is a string and is a valid JSON, then check if the key exists in the JSON
|
||||
filter = fmt.Sprintf(`((type(body) == "string" && isJSON(body)) && %s %s %s)`, exprFormattedValue(strings.TrimPrefix(v.Key.Key, "body.")), logOperatorsToExpr[v.Operator], "fromJSON(body)")
|
||||
|
||||
// if body is a map, then check if the key exists in the map
|
||||
operator := v3.FilterOperatorNotEqual
|
||||
if v.Operator == v3.FilterOperatorNotExists {
|
||||
operator = v3.FilterOperatorEqual
|
||||
}
|
||||
nilCheckFilter := fmt.Sprintf("%s %s nil", v.Key.Key, logOperatorsToExpr[operator])
|
||||
|
||||
// join the two filters with OR
|
||||
filter = fmt.Sprintf(`(%s or (type(body) == "map" && (%s)))`, filter, nilCheckFilter)
|
||||
} else if typ := getTypeName(v.Key.Type); typ != "" {
|
||||
filter = fmt.Sprintf("%s %s %s", exprFormattedValue(v.Key.Key), logOperatorsToExpr[v.Operator], typ)
|
||||
} else {
|
||||
// if type of key is not available; is considered as TOP LEVEL key in OTEL Log Data model hence
|
||||
// switch Exist and Not Exists operators with NOT EQUAL and EQUAL respectively
|
||||
operator := v3.FilterOperatorNotEqual
|
||||
if v.Operator == v3.FilterOperatorNotExists {
|
||||
operator = v3.FilterOperatorEqual
|
||||
}
|
||||
|
||||
filter = fmt.Sprintf("%s %s nil", v.Key.Key, logOperatorsToExpr[operator])
|
||||
// if body is a map, then check if the key exists in the map
|
||||
operator := qbtypes.FilterOperatorNotEqual
|
||||
if c.Op == qbtypes.FilterOperatorNotExists {
|
||||
operator = qbtypes.FilterOperatorEqual
|
||||
}
|
||||
nilCheckFilter := fmt.Sprintf("%s.%s %s nil", c.Key.FieldContext.StringValue(), c.Key.Name, logOperatorsToExpr[operator])
|
||||
|
||||
// join the two filters with OR
|
||||
filter = fmt.Sprintf(`(%s or (type(body) == "map" && (%s)))`, jsonMembership, nilCheckFilter)
|
||||
case telemetrytypes.FieldContextAttribute, telemetrytypes.FieldContextResource:
|
||||
// Example: "http.method" in attributes
|
||||
target := "resource"
|
||||
if c.Key.FieldContext == telemetrytypes.FieldContextAttribute {
|
||||
target = "attributes"
|
||||
}
|
||||
filter = fmt.Sprintf("%q %s %s", c.Key.Name, logOperatorsToExpr[c.Op], target)
|
||||
default:
|
||||
filter = fmt.Sprintf("%s %s %s", name, logOperatorsToExpr[v.Operator], exprFormattedValue(v.Value))
|
||||
|
||||
if v.Operator == v3.FilterOperatorContains || v.Operator == v3.FilterOperatorNotContains {
|
||||
// `contains` and `ncontains` should be case insensitive to match how they work when querying logs.
|
||||
filter = fmt.Sprintf(
|
||||
"lower(%s) %s lower(%s)",
|
||||
name, logOperatorsToExpr[v.Operator], exprFormattedValue(v.Value),
|
||||
)
|
||||
}
|
||||
|
||||
// Avoid running operators on nil values
|
||||
if v.Operator != v3.FilterOperatorEqual && v.Operator != v3.FilterOperatorNotEqual {
|
||||
filter = fmt.Sprintf("%s != nil && %s", name, filter)
|
||||
// if type of key is not available; is considered as TOP LEVEL key in OTEL Log Data model hence
|
||||
// switch Exist and Not Exists operators with NOT EQUAL and EQUAL respectively
|
||||
operator := qbtypes.FilterOperatorNotEqual
|
||||
if c.Op == qbtypes.FilterOperatorNotExists {
|
||||
operator = qbtypes.FilterOperatorEqual
|
||||
}
|
||||
filter = fmt.Sprintf("%s %s nil", c.Key.Name, logOperatorsToExpr[operator])
|
||||
}
|
||||
default:
|
||||
filter = fmt.Sprintf("%s %s %s", getName(c.Key), logOperatorsToExpr[c.Op], value)
|
||||
if c.Op == qbtypes.FilterOperatorContains || c.Op == qbtypes.FilterOperatorNotContains {
|
||||
// `contains` and `ncontains` should be case insensitive to match how they work when querying logs.
|
||||
filter = fmt.Sprintf(
|
||||
"lower(%s) %s lower(%s)",
|
||||
getName(c.Key), logOperatorsToExpr[c.Op], value,
|
||||
)
|
||||
}
|
||||
|
||||
// check if the filter is a correct expression language
|
||||
_, err := expr.Compile(filter)
|
||||
if err != nil {
|
||||
return "", err
|
||||
// Avoid running operators on nil values
|
||||
if c.Op != qbtypes.FilterOperatorEqual && c.Op != qbtypes.FilterOperatorNotEqual {
|
||||
filter = fmt.Sprintf("%s != nil && %s", getName(c.Key), filter)
|
||||
}
|
||||
res = append(res, filter)
|
||||
}
|
||||
|
||||
// check the final filter
|
||||
q := strings.Join(res, " "+strings.ToLower(filters.Operator)+" ")
|
||||
_, err := expr.Compile(q)
|
||||
_, err := expr.Compile(filter)
|
||||
if err != nil {
|
||||
return "", errors.WrapInternalf(err, CodeExprCompilationFailed, "failed to compile expression: %s", q)
|
||||
return "", err
|
||||
}
|
||||
return filter, nil
|
||||
}
|
||||
|
||||
// Parse converts the QB filter Expression (query builder expression string) into
|
||||
// the Expr expression string used by the collector. It parses the QB expression
|
||||
// into a FilterExprNode tree, then serializes that tree to the Expr dialect.
|
||||
func Parse(filter *qbtypes.Filter) (string, error) {
|
||||
if filter == nil || strings.TrimSpace(filter.Expression) == "" {
|
||||
return "", nil
|
||||
}
|
||||
node, err := querybuilder.ParseFilterExpr(filter.Expression)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if node == nil {
|
||||
return "", errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid filter expression; failed to extract filter expression tree")
|
||||
}
|
||||
|
||||
return q, nil
|
||||
for _, condition := range node.Flatten() {
|
||||
if condition.Key == nil {
|
||||
return "", errors.NewInvalidInputf(errors.CodeInternal, "condition has no key")
|
||||
}
|
||||
|
||||
_, found := telemetrylogs.IntrinsicFields[condition.Key.Name]
|
||||
if condition.Key.FieldContext == telemetrytypes.FieldContextUnspecified && !found {
|
||||
return "", errors.NewInvalidInputf(errors.CodeInvalidInput, "field %q in filter expression must include a context prefix (attribute., resource., body.) OR can be one of the following fields: %v", condition.Key.Name, maps.Keys(telemetrylogs.IntrinsicFields))
|
||||
}
|
||||
|
||||
if condition.Op == qbtypes.FilterOperatorRegexp || condition.Op == qbtypes.FilterOperatorNotRegexp {
|
||||
switch condition.Value.(type) {
|
||||
case string:
|
||||
if _, err := regexp.Compile(condition.Value.(string)); err != nil {
|
||||
return "", errors.NewInvalidInputf(errors.CodeInvalidInput, "value for regex operator must be a valid regex")
|
||||
}
|
||||
default:
|
||||
return "", errors.NewInvalidInputf(errors.CodeInvalidInput, "value for regex operator must be a string")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nodeToExpr(node)
|
||||
}
|
||||
|
||||
func nodeToExpr(node *qbtypes.FilterExprNode) (string, error) {
|
||||
if node == nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
var result string
|
||||
|
||||
switch node.Op {
|
||||
case qbtypes.LogicalOpLeaf:
|
||||
var parts []string
|
||||
for _, c := range node.Conditions {
|
||||
s, err := parseCondition(c)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
parts = append(parts, s)
|
||||
}
|
||||
if len(parts) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
// For a simple leaf, just join conditions with AND without wrapping
|
||||
// the whole clause in parentheses
|
||||
result = strings.Join(parts, " and ")
|
||||
case qbtypes.LogicalOpAnd:
|
||||
var parts []string
|
||||
for _, child := range node.Children {
|
||||
if child == nil {
|
||||
continue
|
||||
}
|
||||
s, err := nodeToExpr(child)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// When mixing AND/OR, we need parentheses around any OR child to
|
||||
// preserve the intended precedence: (a and (b or c)).
|
||||
if child.Op == qbtypes.LogicalOpOr {
|
||||
s = fmt.Sprintf("(%s)", s)
|
||||
}
|
||||
parts = append(parts, s)
|
||||
}
|
||||
if len(parts) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
result = strings.Join(parts, " and ")
|
||||
case qbtypes.LogicalOpOr:
|
||||
var parts []string
|
||||
for _, child := range node.Children {
|
||||
if child == nil {
|
||||
continue
|
||||
}
|
||||
s, err := nodeToExpr(child)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// When mixing AND/OR, we need parentheses around any AND child to
|
||||
// preserve the intended precedence: ((a and b) or c).
|
||||
if child.Op == qbtypes.LogicalOpAnd {
|
||||
s = fmt.Sprintf("(%s)", s)
|
||||
}
|
||||
parts = append(parts, s)
|
||||
}
|
||||
if len(parts) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
result = strings.Join(parts, " or ")
|
||||
default:
|
||||
return "", errors.NewInvalidInputf(errors.CodeInvalidInput, "unsupported logical op: %s", node.Op)
|
||||
}
|
||||
|
||||
if node.Negated {
|
||||
// Apply a leading NOT to this subtree. Always wrap the underlying
|
||||
// expression in parentheses to preserve the intended precedence.
|
||||
return fmt.Sprintf("not (%s)", result), nil
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func exprFormattedValue(v interface{}) string {
|
||||
@@ -135,13 +254,12 @@ func exprFormattedValue(v interface{}) string {
|
||||
case uint8, uint16, uint32, uint64, int, int8, int16, int32, int64:
|
||||
return fmt.Sprintf("%d", x)
|
||||
case float32, float64:
|
||||
return fmt.Sprintf("%f", x)
|
||||
return fmt.Sprintf("%v", x)
|
||||
case string:
|
||||
return fmt.Sprintf("\"%s\"", quoteEscapedString(x))
|
||||
case bool:
|
||||
return fmt.Sprintf("%v", x)
|
||||
|
||||
case []interface{}:
|
||||
case []any:
|
||||
if len(x) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"testing"
|
||||
|
||||
signozstanzahelper "github.com/SigNoz/signoz-otel-collector/processor/signozlogspipelineprocessor/stanza/operator/helper"
|
||||
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/expr-lang/expr/vm"
|
||||
"github.com/open-telemetry/opentelemetry-collector-contrib/pkg/stanza/entry"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -13,183 +13,191 @@ import (
|
||||
func TestParseExpression(t *testing.T) {
|
||||
var testCases = []struct {
|
||||
Name string
|
||||
Query *v3.FilterSet
|
||||
Query *qbtypes.Filter
|
||||
Expr string
|
||||
ExpectError bool
|
||||
}{
|
||||
{
|
||||
Name: "equal",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "key", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Value: "checkbody", Operator: "="},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "attribute.key = 'checkbody'",
|
||||
},
|
||||
Expr: `attributes["key"] == "checkbody"`,
|
||||
},
|
||||
{
|
||||
Name: "NOT equal (unary NOT)",
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "NOT (attribute.key = 'checkbody')",
|
||||
},
|
||||
Expr: `not (attributes["key"] == "checkbody")`,
|
||||
},
|
||||
{
|
||||
Name: "not equal",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "key", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Value: "checkbody", Operator: "!="},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "attribute.key != 'checkbody'",
|
||||
},
|
||||
Expr: `attributes["key"] != "checkbody"`,
|
||||
},
|
||||
{
|
||||
Name: "less than",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "key", DataType: v3.AttributeKeyDataTypeInt64, Type: v3.AttributeKeyTypeTag}, Value: 10, Operator: "<"},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "attribute.key < 10",
|
||||
},
|
||||
Expr: `attributes["key"] != nil && attributes["key"] < 10`,
|
||||
},
|
||||
{
|
||||
Name: "greater than",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "key", DataType: v3.AttributeKeyDataTypeInt64, Type: v3.AttributeKeyTypeTag}, Value: 10, Operator: ">"},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "attribute.key > 10",
|
||||
},
|
||||
Expr: `attributes["key"] != nil && attributes["key"] > 10`,
|
||||
},
|
||||
{
|
||||
Name: "less than equal",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "key", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Value: 10, Operator: "<="},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "attribute.key <= 10",
|
||||
},
|
||||
Expr: `attributes["key"] != nil && attributes["key"] <= 10`,
|
||||
},
|
||||
{
|
||||
Name: "greater than equal",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "key", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Value: 10, Operator: ">="},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "attribute.key >= 10",
|
||||
},
|
||||
Expr: `attributes["key"] != nil && attributes["key"] >= 10`,
|
||||
},
|
||||
// case sensitive
|
||||
{
|
||||
Name: "body contains",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "body", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true}, Value: "checkbody", Operator: "contains"},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "body contains 'checkbody'",
|
||||
},
|
||||
Expr: `body != nil && lower(body) contains lower("checkbody")`,
|
||||
},
|
||||
{
|
||||
Name: "body.log.message exists",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "body.log.message", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true}, Value: "", Operator: "exists"},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "body.log.message exists",
|
||||
},
|
||||
Expr: `(((type(body) == "string" && isJSON(body)) && "log.message" in fromJSON(body)) or (type(body) == "map" && (body.log.message != nil)))`,
|
||||
},
|
||||
{
|
||||
Name: "body.log.message not exists",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "body.log.message", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true}, Value: "", Operator: "nexists"},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "body.log.message not exists",
|
||||
},
|
||||
Expr: `(((type(body) == "string" && isJSON(body)) && "log.message" not in fromJSON(body)) or (type(body) == "map" && (body.log.message == nil)))`,
|
||||
},
|
||||
{
|
||||
Name: "body ncontains",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "body", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true}, Value: "checkbody", Operator: "ncontains"},
|
||||
}},
|
||||
Name: "body NOT CONTAINS",
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "body NOT CONTAINS 'checkbody'",
|
||||
},
|
||||
Expr: `body != nil && lower(body) not contains lower("checkbody")`,
|
||||
},
|
||||
{
|
||||
Name: "body regex",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "body", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true}, Value: "[0-1]+regex$", Operator: "regex"},
|
||||
}},
|
||||
Name: "body REGEXP",
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "body REGEXP '[0-1]+regex$'",
|
||||
},
|
||||
Expr: `body != nil && body matches "[0-1]+regex$"`,
|
||||
},
|
||||
{
|
||||
Name: "body not regex",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "body", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true}, Value: "[0-1]+regex$", Operator: "nregex"},
|
||||
}},
|
||||
Name: "body NOT REGEXP",
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "body NOT REGEXP '[0-1]+regex$'",
|
||||
},
|
||||
Expr: `body != nil && body not matches "[0-1]+regex$"`,
|
||||
},
|
||||
{
|
||||
Name: "regex with escape characters",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "body", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true}, Value: `^Executing \[\S+@\S+:[0-9]+\] \S+".*`, Operator: "regex"},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "body REGEXP '^Executing \\[\\S+@\\S+:[0-9]+\\] \\S+\".*'",
|
||||
},
|
||||
Expr: `body != nil && body matches "^Executing \\[\\S+@\\S+:[0-9]+\\] \\S+\".*"`,
|
||||
},
|
||||
{
|
||||
Name: "invalid regex",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "body", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true}, Value: "[0-9]++", Operator: "nregex"},
|
||||
}},
|
||||
Expr: `body != nil && lower(body) not matches "[0-9]++"`,
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "body not REGEXP '[0-9]++'",
|
||||
},
|
||||
ExpectError: true,
|
||||
},
|
||||
{
|
||||
Name: "in",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "key", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Value: []any{1, 2, 3, 4}, Operator: "in"},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "attribute.key in [1,2,3,4]",
|
||||
},
|
||||
Expr: `attributes["key"] != nil && attributes["key"] in [1,2,3,4]`,
|
||||
},
|
||||
{
|
||||
Name: "not in",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "key", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Value: []any{"1", "2"}, Operator: "nin"},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "attribute.key not in ['1','2']",
|
||||
},
|
||||
Expr: `attributes["key"] != nil && attributes["key"] not in ['1','2']`,
|
||||
},
|
||||
{
|
||||
Name: "exists",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "key", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Operator: "exists"},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "attribute.key exists",
|
||||
},
|
||||
Expr: `"key" in attributes`,
|
||||
},
|
||||
{
|
||||
Name: "not exists",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "key", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Operator: "nexists"},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "attribute.key not exists",
|
||||
},
|
||||
Expr: `"key" not in attributes`,
|
||||
},
|
||||
{
|
||||
Name: "trace_id not exists",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "trace_id", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true}, Value: "", Operator: "nexists"},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "trace_id not exists",
|
||||
},
|
||||
Expr: `trace_id == nil`,
|
||||
},
|
||||
{
|
||||
Name: "trace_id exists",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "trace_id", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true}, Value: "", Operator: "exists"},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "trace_id exists",
|
||||
},
|
||||
Expr: `trace_id != nil`,
|
||||
},
|
||||
{
|
||||
Name: "span_id not exists",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "span_id", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true}, Value: "", Operator: "nexists"},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "span_id not exists",
|
||||
},
|
||||
Expr: `span_id == nil`,
|
||||
},
|
||||
{
|
||||
Name: "span_id exists",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "span_id", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true}, Value: "", Operator: "exists"},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "span_id exists",
|
||||
},
|
||||
Expr: `span_id != nil`,
|
||||
},
|
||||
{
|
||||
Name: "Multi filter",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "key", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Value: 10, Operator: "<="},
|
||||
{Key: v3.AttributeKey{Key: "body", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true}, Value: "[0-1]+regex$", Operator: "nregex"},
|
||||
{Key: v3.AttributeKey{Key: "key", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Operator: "nexists"},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "attribute.key <= 10 and body not regexp '[0-1]+regex$' and attribute.key not exists",
|
||||
},
|
||||
Expr: `attributes["key"] != nil && attributes["key"] <= 10 and body != nil && body not matches "[0-1]+regex$" and "key" not in attributes`,
|
||||
},
|
||||
{
|
||||
Name: "incorrect multi filter",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "key", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Value: 10, Operator: "<="},
|
||||
{Key: v3.AttributeKey{Key: "body", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true}, Value: "[0-9]++", Operator: "nregex"},
|
||||
{Key: v3.AttributeKey{Key: "key", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Operator: "nexists"},
|
||||
}},
|
||||
Expr: `attributes["key"] != nil && attributes["key"] <= 10 and body not matches "[0-9]++" and "key" not in attributes`,
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "attribute.key <= 10 and body not regexp '[0-9]++' and attribute.key not exists",
|
||||
},
|
||||
ExpectError: true,
|
||||
},
|
||||
{
|
||||
Name: "attributes. is unsupported",
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "attributes.key = 'checkbody'",
|
||||
},
|
||||
ExpectError: true,
|
||||
},
|
||||
}
|
||||
@@ -267,248 +275,241 @@ func TestExpressionVSEntry(t *testing.T) {
|
||||
|
||||
var testCases = []struct {
|
||||
Name string
|
||||
Query *v3.FilterSet
|
||||
Query *qbtypes.Filter
|
||||
ExpectedMatches []int
|
||||
}{
|
||||
{
|
||||
Name: "resource equal (env)",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "env", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeResource}, Value: "prod", Operator: "="},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "resource.env = 'prod'",
|
||||
},
|
||||
ExpectedMatches: []int{0, 1, 4, 5, 9, 11, 13, 14, 17, 18},
|
||||
},
|
||||
{
|
||||
Name: "NOT resource equal (unary NOT)",
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "NOT (resource.env = 'prod')",
|
||||
},
|
||||
ExpectedMatches: []int{2, 3, 6, 7, 8, 10, 12, 15, 16, 19, 20},
|
||||
},
|
||||
{
|
||||
Name: "resource not equal (env)",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "env", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeResource}, Value: "prod", Operator: "!="},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "resource.env != 'prod'",
|
||||
},
|
||||
ExpectedMatches: []int{2, 3, 6, 7, 8, 10, 12, 15, 16, 19, 20},
|
||||
},
|
||||
{
|
||||
Name: "attribute less than (numeric)",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "count", DataType: v3.AttributeKeyDataTypeInt64, Type: v3.AttributeKeyTypeTag}, Value: 8, Operator: "<"},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "attribute.count < 8",
|
||||
},
|
||||
ExpectedMatches: []int{4},
|
||||
},
|
||||
{
|
||||
Name: "attribute greater than (numeric)",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "count", DataType: v3.AttributeKeyDataTypeInt64, Type: v3.AttributeKeyTypeTag}, Value: 8, Operator: ">"},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "attribute.count > 8",
|
||||
},
|
||||
ExpectedMatches: []int{5},
|
||||
},
|
||||
{
|
||||
Name: "body contains (case insensitive)",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "body", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true}, Value: "checkbody", Operator: "contains"},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "body contains 'checkbody'",
|
||||
},
|
||||
ExpectedMatches: []int{2, 9, 10, 16},
|
||||
},
|
||||
{
|
||||
Name: "body ncontains",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "body", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true}, Value: "checkbody", Operator: "ncontains"},
|
||||
}},
|
||||
Name: "body NOT CONTAINS",
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "body NOT CONTAINS 'checkbody'",
|
||||
},
|
||||
ExpectedMatches: []int{0, 1, 3, 4, 5, 6, 7, 8, 11, 12, 13, 14, 15, 17},
|
||||
},
|
||||
{
|
||||
Name: "body.msg (case insensitive)",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "body.msg", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: false}, Value: "checkbody", Operator: "contains"},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "body.msg contains 'checkbody'",
|
||||
},
|
||||
ExpectedMatches: []int{2, 9, 10, 18},
|
||||
},
|
||||
{
|
||||
Name: "body regex",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "body", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true}, Value: "[0-1]+regex", Operator: "regex"},
|
||||
}},
|
||||
Name: "body REGEXP",
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "body REGEXP '[0-1]+regex'",
|
||||
},
|
||||
ExpectedMatches: []int{4},
|
||||
},
|
||||
{
|
||||
Name: "body not regex",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "body", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true}, Value: "[0-1]+regex", Operator: "nregex"},
|
||||
}},
|
||||
Name: "body NOT REGEXP",
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "body NOT REGEXP '[0-1]+regex'",
|
||||
},
|
||||
ExpectedMatches: []int{0, 1, 2, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17},
|
||||
},
|
||||
// body.log.message exists/nexists: expr checks "log.message" in fromJSON(body); nested key
|
||||
// semantics depend on signoz stanza helper. Omitted here to avoid coupling to env shape.
|
||||
{
|
||||
Name: "body top-level key exists (body.msg)",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "body.msg", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true}, Value: "", Operator: "exists"},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "body.msg exists",
|
||||
},
|
||||
ExpectedMatches: []int{0, 1, 2, 3, 4, 5, 9, 10, 11, 12, 18},
|
||||
},
|
||||
{
|
||||
Name: "body top-level key not exists (body.missing)",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "body.missing", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true}, Value: "", Operator: "nexists"},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "body.missing not exists",
|
||||
},
|
||||
ExpectedMatches: []int{0, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 18, 20},
|
||||
},
|
||||
{
|
||||
Name: "attribute exists",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "service", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Operator: "exists"},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "attribute.service exists",
|
||||
},
|
||||
ExpectedMatches: []int{6, 7, 8, 15},
|
||||
},
|
||||
{
|
||||
Name: "attribute not exists",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "service", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Operator: "nexists"},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "attribute.service not exists",
|
||||
},
|
||||
ExpectedMatches: []int{0, 1, 2, 3, 4, 5, 9, 10, 11, 12, 13, 14, 16, 17, 18, 19, 20},
|
||||
},
|
||||
{
|
||||
Name: "trace_id exists",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "trace_id", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true}, Value: "", Operator: "exists"},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "trace_id exists",
|
||||
},
|
||||
ExpectedMatches: []int{1, 2, 5, 7, 12, 15, 19},
|
||||
},
|
||||
{
|
||||
Name: "trace_id not exists",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "trace_id", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true}, Value: "", Operator: "nexists"},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "trace_id not exists",
|
||||
},
|
||||
ExpectedMatches: []int{0, 3, 4, 6, 8, 9, 10, 11, 13, 14, 16, 17, 18, 20},
|
||||
},
|
||||
{
|
||||
Name: "span_id exists",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "span_id", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true}, Value: "", Operator: "exists"},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "span_id exists",
|
||||
},
|
||||
ExpectedMatches: []int{1, 3, 5, 12, 17, 20},
|
||||
},
|
||||
{
|
||||
Name: "span_id not exists",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "span_id", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true}, Value: "", Operator: "nexists"},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "span_id not exists",
|
||||
},
|
||||
ExpectedMatches: []int{0, 2, 4, 6, 7, 8, 9, 10, 11, 13, 14, 15, 16, 18, 19},
|
||||
},
|
||||
{
|
||||
Name: "in (attribute in list)",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "level", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Value: []any{"info", "error"}, Operator: "in"},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "attribute.level in ['info', 'error']",
|
||||
},
|
||||
ExpectedMatches: []int{0, 1, 2, 14, 16, 20},
|
||||
},
|
||||
{
|
||||
Name: "not in (attribute not in list)",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "level", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Value: []any{"error", "warn"}, Operator: "nin"},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "attribute.level not in ['error', 'warn']",
|
||||
},
|
||||
ExpectedMatches: []int{0, 2, 3, 16, 20},
|
||||
},
|
||||
{
|
||||
Name: "multi filter AND",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "env", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeResource}, Value: "staging", Operator: "="},
|
||||
{Key: v3.AttributeKey{Key: "level", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Value: "info", Operator: "="},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "resource.env = 'staging' and attribute.level = 'info'",
|
||||
},
|
||||
ExpectedMatches: []int{2, 16},
|
||||
},
|
||||
{
|
||||
Name: "multi filter AND (two attributes)",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "service", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Value: "auth", Operator: "="},
|
||||
{Key: v3.AttributeKey{Key: "level", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Operator: "nexists"},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "attribute.service = 'auth' and attribute.level not exists",
|
||||
},
|
||||
ExpectedMatches: []int{6, 7},
|
||||
},
|
||||
// Multi-filter variations: body + attribute, three conditions, trace/span + attribute
|
||||
{
|
||||
Name: "multi filter AND body contains + attribute",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "body", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true}, Value: "Connection", Operator: "contains"},
|
||||
{Key: v3.AttributeKey{Key: "env", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeResource}, Value: "prod", Operator: "="},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "body contains 'Connection' and resource.env = 'prod'",
|
||||
},
|
||||
ExpectedMatches: []int{14},
|
||||
},
|
||||
{
|
||||
Name: "multi filter AND body contains + service",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "body", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true}, Value: "login", Operator: "contains"},
|
||||
{Key: v3.AttributeKey{Key: "service", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Value: "auth", Operator: "="},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "body contains 'login' and attribute.service = 'auth'",
|
||||
},
|
||||
ExpectedMatches: []int{6, 15},
|
||||
},
|
||||
{
|
||||
Name: "multi filter AND env + level (prod error)",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "env", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeResource}, Value: "prod", Operator: "="},
|
||||
{Key: v3.AttributeKey{Key: "level", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Value: "error", Operator: "="},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "resource.env = 'prod' and attribute.level = 'error'",
|
||||
},
|
||||
ExpectedMatches: []int{1, 14},
|
||||
},
|
||||
{
|
||||
Name: "multi filter AND three conditions (staging + checkbody + info)",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "env", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeResource}, Value: "staging", Operator: "="},
|
||||
{Key: v3.AttributeKey{Key: "body", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true}, Value: "checkbody", Operator: "contains"},
|
||||
{Key: v3.AttributeKey{Key: "level", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Value: "info", Operator: "="},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "resource.env = 'staging' and body contains 'checkbody' and attribute.level = 'info'",
|
||||
},
|
||||
ExpectedMatches: []int{2, 16},
|
||||
},
|
||||
{
|
||||
Name: "multi filter AND trace_id exists + body contains",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "trace_id", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true}, Value: "", Operator: "exists"},
|
||||
{Key: v3.AttributeKey{Key: "body", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true}, Value: "checkbody", Operator: "contains"},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "trace_id exists and body contains 'checkbody'",
|
||||
},
|
||||
ExpectedMatches: []int{2},
|
||||
},
|
||||
{
|
||||
Name: "multi filter AND span_id nexists + service auth",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "span_id", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true}, Value: "", Operator: "nexists"},
|
||||
{Key: v3.AttributeKey{Key: "service", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Value: "auth", Operator: "="},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "span_id not exists and attribute.service = 'auth'",
|
||||
},
|
||||
ExpectedMatches: []int{6, 7, 15},
|
||||
},
|
||||
{
|
||||
Name: "multi filter AND body regex + attribute",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "body", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true}, Value: "[0-1]+regex", Operator: "regex"},
|
||||
{Key: v3.AttributeKey{Key: "code", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Value: "200", Operator: "="},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "body regexp '[0-1]+regex' and attribute.code = '200'",
|
||||
},
|
||||
ExpectedMatches: []int{4},
|
||||
},
|
||||
{
|
||||
Name: "multi filter AND no trace_id + no span_id + env prod",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "trace_id", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true}, Value: "", Operator: "nexists"},
|
||||
{Key: v3.AttributeKey{Key: "span_id", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true}, Value: "", Operator: "nexists"},
|
||||
{Key: v3.AttributeKey{Key: "env", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeResource}, Value: "prod", Operator: "="},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "trace_id not exists and span_id not exists and resource.env = 'prod'",
|
||||
},
|
||||
ExpectedMatches: []int{0, 4, 9, 11, 13, 14, 18},
|
||||
},
|
||||
{
|
||||
Name: "multi filter AND level warn + body contains",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "level", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Value: "warn", Operator: "="},
|
||||
{Key: v3.AttributeKey{Key: "body", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true}, Value: "disk", Operator: "contains"},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "attribute.level = 'warn' and body contains 'disk'",
|
||||
},
|
||||
ExpectedMatches: []int{17},
|
||||
},
|
||||
{
|
||||
Name: "no matches (attribute value not present)",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "env", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeResource}, Value: "never", Operator: "="},
|
||||
}},
|
||||
Name: "no matches (resource value not present)",
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "resource.env = 'never'",
|
||||
},
|
||||
ExpectedMatches: []int{},
|
||||
},
|
||||
{
|
||||
Name: "attribute equal and trace_id exists",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "code", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Value: "404", Operator: "="},
|
||||
{Key: v3.AttributeKey{Key: "trace_id", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true}, Value: "", Operator: "exists"},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "attribute.code = '404' and trace_id exists",
|
||||
},
|
||||
ExpectedMatches: []int{5},
|
||||
},
|
||||
}
|
||||
|
||||
390
pkg/querybuilder/filter_expr_parse.go
Normal file
390
pkg/querybuilder/filter_expr_parse.go
Normal file
@@ -0,0 +1,390 @@
|
||||
package querybuilder
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
grammar "github.com/SigNoz/signoz/pkg/parser/grammar"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/antlr4-go/antlr/v4"
|
||||
)
|
||||
|
||||
// ParseFilterExpr parses a v5 filter expression and returns a logical
|
||||
// tree that preserves full boolean structure (AND/OR, parentheses, NOT) as
|
||||
// well as the individual conditions (keys, operator, values).
|
||||
//
|
||||
// This can be reused by callers that need deeper introspection into the logic
|
||||
// of the filter expression without constructing any SQL or query-engine
|
||||
// specific structures.
|
||||
func ParseFilterExpr(expr string) (*qbtypes.FilterExprNode, error) {
|
||||
if strings.TrimSpace(expr) == "" {
|
||||
return qbtypes.NewEmptyFilterExprNode(), nil
|
||||
}
|
||||
|
||||
// Setup the ANTLR parsing pipeline (same grammar as PrepareWhereClause).
|
||||
input := antlr.NewInputStream(expr)
|
||||
lexer := grammar.NewFilterQueryLexer(input)
|
||||
|
||||
lexerErrorListener := NewErrorListener()
|
||||
lexer.RemoveErrorListeners()
|
||||
lexer.AddErrorListener(lexerErrorListener)
|
||||
|
||||
tokens := antlr.NewCommonTokenStream(lexer, 0)
|
||||
parserErrorListener := NewErrorListener()
|
||||
parser := grammar.NewFilterQueryParser(tokens)
|
||||
parser.RemoveErrorListeners()
|
||||
parser.AddErrorListener(parserErrorListener)
|
||||
|
||||
tree := parser.Query()
|
||||
|
||||
// Handle syntax errors
|
||||
if len(parserErrorListener.SyntaxErrors) > 0 {
|
||||
combinedErrors := errors.Newf(
|
||||
errors.TypeInvalidInput,
|
||||
errors.CodeInvalidInput,
|
||||
"Found %d syntax errors while parsing the search expression.",
|
||||
len(parserErrorListener.SyntaxErrors),
|
||||
)
|
||||
additionals := make([]string, 0, len(parserErrorListener.SyntaxErrors))
|
||||
for _, err := range parserErrorListener.SyntaxErrors {
|
||||
if err.Error() != "" {
|
||||
additionals = append(additionals, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
return nil, combinedErrors.WithAdditional(additionals...)
|
||||
}
|
||||
|
||||
visitor := &filterTreeVisitor{}
|
||||
rootAny := visitor.Visit(tree)
|
||||
if len(visitor.errors) > 0 {
|
||||
combinedErrors := errors.Newf(
|
||||
errors.TypeInvalidInput,
|
||||
errors.CodeInvalidInput,
|
||||
"Found %d errors while parsing the search expression.",
|
||||
len(visitor.errors),
|
||||
)
|
||||
return nil, combinedErrors.WithAdditional(visitor.errors...)
|
||||
}
|
||||
|
||||
root, _ := rootAny.(*qbtypes.FilterExprNode)
|
||||
return root, nil
|
||||
}
|
||||
|
||||
// filterTreeVisitor builds a FilterExprNode tree from the parse tree.
|
||||
type filterTreeVisitor struct {
|
||||
errors []string
|
||||
}
|
||||
|
||||
// Visit dispatches based on node type.
|
||||
func (v *filterTreeVisitor) Visit(tree antlr.ParseTree) any {
|
||||
if tree == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch t := tree.(type) {
|
||||
case *grammar.QueryContext:
|
||||
return v.VisitQuery(t)
|
||||
case *grammar.ExpressionContext:
|
||||
return v.VisitExpression(t)
|
||||
case *grammar.OrExpressionContext:
|
||||
return v.VisitOrExpression(t)
|
||||
case *grammar.AndExpressionContext:
|
||||
return v.VisitAndExpression(t)
|
||||
case *grammar.UnaryExpressionContext:
|
||||
return v.VisitUnaryExpression(t)
|
||||
case *grammar.PrimaryContext:
|
||||
return v.VisitPrimary(t)
|
||||
case *grammar.ComparisonContext:
|
||||
return v.VisitComparison(t)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (v *filterTreeVisitor) VisitQuery(ctx *grammar.QueryContext) any {
|
||||
return v.Visit(ctx.Expression())
|
||||
}
|
||||
|
||||
func (v *filterTreeVisitor) VisitExpression(ctx *grammar.ExpressionContext) any {
|
||||
return v.Visit(ctx.OrExpression())
|
||||
}
|
||||
|
||||
func (v *filterTreeVisitor) VisitOrExpression(ctx *grammar.OrExpressionContext) any {
|
||||
andExprs := ctx.AllAndExpression()
|
||||
children := make([]*qbtypes.FilterExprNode, 0, len(andExprs))
|
||||
|
||||
for _, andExpr := range andExprs {
|
||||
if node, ok := v.Visit(andExpr).(*qbtypes.FilterExprNode); ok && node != nil {
|
||||
children = append(children, node)
|
||||
}
|
||||
}
|
||||
|
||||
if len(children) == 0 {
|
||||
return nil
|
||||
}
|
||||
if len(children) == 1 {
|
||||
return children[0]
|
||||
}
|
||||
|
||||
return &qbtypes.FilterExprNode{
|
||||
Op: qbtypes.LogicalOpOr,
|
||||
Children: children,
|
||||
}
|
||||
}
|
||||
|
||||
func (v *filterTreeVisitor) VisitAndExpression(ctx *grammar.AndExpressionContext) any {
|
||||
unaryExprs := ctx.AllUnaryExpression()
|
||||
children := make([]*qbtypes.FilterExprNode, 0, len(unaryExprs))
|
||||
|
||||
for _, unary := range unaryExprs {
|
||||
if node, ok := v.Visit(unary).(*qbtypes.FilterExprNode); ok && node != nil {
|
||||
children = append(children, node)
|
||||
}
|
||||
}
|
||||
|
||||
if len(children) == 0 {
|
||||
return nil
|
||||
}
|
||||
if len(children) == 1 {
|
||||
return children[0]
|
||||
}
|
||||
|
||||
return &qbtypes.FilterExprNode{
|
||||
Op: qbtypes.LogicalOpAnd,
|
||||
Children: children,
|
||||
}
|
||||
}
|
||||
|
||||
func (v *filterTreeVisitor) VisitUnaryExpression(ctx *grammar.UnaryExpressionContext) any {
|
||||
node, _ := v.Visit(ctx.Primary()).(*qbtypes.FilterExprNode)
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if ctx.NOT() != nil {
|
||||
node.Negated = !node.Negated
|
||||
}
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
func (v *filterTreeVisitor) VisitPrimary(ctx *grammar.PrimaryContext) any {
|
||||
switch {
|
||||
case ctx.OrExpression() != nil:
|
||||
return v.Visit(ctx.OrExpression())
|
||||
case ctx.Comparison() != nil:
|
||||
return v.Visit(ctx.Comparison())
|
||||
default:
|
||||
// We intentionally ignore FullText/FunctionCall here for the tree
|
||||
// representation. They can be added later if needed.
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// VisitComparison builds a leaf node with a single ParsedFilterCondition.
|
||||
func (v *filterTreeVisitor) VisitComparison(ctx *grammar.ComparisonContext) any {
|
||||
key := v.buildKey(ctx.Key())
|
||||
if key == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Handle EXISTS specially
|
||||
if ctx.EXISTS() != nil {
|
||||
op := qbtypes.FilterOperatorExists
|
||||
if ctx.NOT() != nil {
|
||||
op = qbtypes.FilterOperatorNotExists
|
||||
}
|
||||
|
||||
return &qbtypes.FilterExprNode{
|
||||
Op: qbtypes.LogicalOpLeaf,
|
||||
Conditions: []qbtypes.FilterCondition{{
|
||||
Key: key,
|
||||
Op: op,
|
||||
Value: nil,
|
||||
}},
|
||||
}
|
||||
}
|
||||
|
||||
// Handle IN / NOT IN
|
||||
if ctx.InClause() != nil || ctx.NotInClause() != nil {
|
||||
values := v.buildValuesFromInClause(ctx.InClause(), ctx.NotInClause())
|
||||
|
||||
op := qbtypes.FilterOperatorIn
|
||||
if ctx.NotInClause() != nil {
|
||||
op = qbtypes.FilterOperatorNotIn
|
||||
}
|
||||
|
||||
return &qbtypes.FilterExprNode{
|
||||
Op: qbtypes.LogicalOpLeaf,
|
||||
Conditions: []qbtypes.FilterCondition{{
|
||||
Key: key,
|
||||
Op: op,
|
||||
Value: values,
|
||||
}},
|
||||
}
|
||||
}
|
||||
|
||||
// Handle BETWEEN / NOT BETWEEN
|
||||
if ctx.BETWEEN() != nil {
|
||||
valuesCtx := ctx.AllValue()
|
||||
if len(valuesCtx) != 2 {
|
||||
v.errors = append(v.errors, "BETWEEN operator requires exactly two values")
|
||||
return nil
|
||||
}
|
||||
|
||||
value1 := v.buildValue(valuesCtx[0])
|
||||
value2 := v.buildValue(valuesCtx[1])
|
||||
|
||||
op := qbtypes.FilterOperatorBetween
|
||||
if ctx.NOT() != nil {
|
||||
op = qbtypes.FilterOperatorNotBetween
|
||||
}
|
||||
|
||||
return &qbtypes.FilterExprNode{
|
||||
Op: qbtypes.LogicalOpLeaf,
|
||||
Conditions: []qbtypes.FilterCondition{{
|
||||
Key: key,
|
||||
Op: op,
|
||||
Value: []any{value1, value2},
|
||||
}},
|
||||
}
|
||||
}
|
||||
|
||||
// All remaining operators have exactly one value.
|
||||
valuesCtx := ctx.AllValue()
|
||||
if len(valuesCtx) == 0 {
|
||||
v.errors = append(v.errors, "comparison operator requires a value")
|
||||
return nil
|
||||
}
|
||||
|
||||
value := v.buildValue(valuesCtx[0])
|
||||
|
||||
var op qbtypes.FilterOperator
|
||||
switch {
|
||||
case ctx.EQUALS() != nil:
|
||||
op = qbtypes.FilterOperatorEqual
|
||||
case ctx.NOT_EQUALS() != nil || ctx.NEQ() != nil:
|
||||
op = qbtypes.FilterOperatorNotEqual
|
||||
case ctx.LT() != nil:
|
||||
op = qbtypes.FilterOperatorLessThan
|
||||
case ctx.LE() != nil:
|
||||
op = qbtypes.FilterOperatorLessThanOrEq
|
||||
case ctx.GT() != nil:
|
||||
op = qbtypes.FilterOperatorGreaterThan
|
||||
case ctx.GE() != nil:
|
||||
op = qbtypes.FilterOperatorGreaterThanOrEq
|
||||
case ctx.LIKE() != nil:
|
||||
op = qbtypes.FilterOperatorLike
|
||||
if ctx.NOT() != nil {
|
||||
op = qbtypes.FilterOperatorNotLike
|
||||
}
|
||||
case ctx.ILIKE() != nil:
|
||||
op = qbtypes.FilterOperatorILike
|
||||
if ctx.NOT() != nil {
|
||||
op = qbtypes.FilterOperatorNotILike
|
||||
}
|
||||
case ctx.REGEXP() != nil:
|
||||
op = qbtypes.FilterOperatorRegexp
|
||||
if ctx.NOT() != nil {
|
||||
op = qbtypes.FilterOperatorNotRegexp
|
||||
}
|
||||
case ctx.CONTAINS() != nil:
|
||||
op = qbtypes.FilterOperatorContains
|
||||
if ctx.NOT() != nil {
|
||||
op = qbtypes.FilterOperatorNotContains
|
||||
}
|
||||
default:
|
||||
v.errors = append(v.errors, fmt.Sprintf("unsupported comparison operator in expression: %s", ctx.GetText()))
|
||||
return nil
|
||||
}
|
||||
|
||||
return &qbtypes.FilterExprNode{
|
||||
Op: qbtypes.LogicalOpLeaf,
|
||||
Conditions: []qbtypes.FilterCondition{{
|
||||
Key: key,
|
||||
Op: op,
|
||||
Value: value,
|
||||
}},
|
||||
}
|
||||
}
|
||||
|
||||
// buildKey turns a key context into a TelemetryFieldKey.
|
||||
func (v *filterTreeVisitor) buildKey(ctx grammar.IKeyContext) *telemetrytypes.TelemetryFieldKey {
|
||||
if ctx == nil {
|
||||
return nil
|
||||
}
|
||||
key := telemetrytypes.GetFieldKeyFromKeyText(ctx.GetText())
|
||||
return &key
|
||||
}
|
||||
|
||||
// buildValuesFromInClause handles the IN/NOT IN value side.
|
||||
func (v *filterTreeVisitor) buildValuesFromInClause(in grammar.IInClauseContext, notIn grammar.INotInClauseContext) []any {
|
||||
var ctxVal any
|
||||
if in != nil {
|
||||
ctxVal = v.visitInClause(in)
|
||||
} else if notIn != nil {
|
||||
ctxVal = v.visitNotInClause(notIn)
|
||||
}
|
||||
|
||||
switch ret := ctxVal.(type) {
|
||||
case []any:
|
||||
return ret
|
||||
case any:
|
||||
if ret != nil {
|
||||
return []any{ret}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *filterTreeVisitor) visitInClause(ctx grammar.IInClauseContext) any {
|
||||
if ctx.ValueList() != nil {
|
||||
return v.visitValueList(ctx.ValueList())
|
||||
}
|
||||
return v.buildValue(ctx.Value())
|
||||
}
|
||||
|
||||
func (v *filterTreeVisitor) visitNotInClause(ctx grammar.INotInClauseContext) any {
|
||||
if ctx.ValueList() != nil {
|
||||
return v.visitValueList(ctx.ValueList())
|
||||
}
|
||||
return v.buildValue(ctx.Value())
|
||||
}
|
||||
|
||||
func (v *filterTreeVisitor) visitValueList(ctx grammar.IValueListContext) any {
|
||||
values := ctx.AllValue()
|
||||
parts := make([]any, 0, len(values))
|
||||
for _, val := range values {
|
||||
parts = append(parts, v.buildValue(val))
|
||||
}
|
||||
return parts
|
||||
}
|
||||
|
||||
// buildValue converts literal values into Go types (string, float64, bool).
|
||||
func (v *filterTreeVisitor) buildValue(ctx grammar.IValueContext) any {
|
||||
switch {
|
||||
case ctx == nil:
|
||||
return nil
|
||||
case ctx.QUOTED_TEXT() != nil:
|
||||
txt := ctx.QUOTED_TEXT().GetText()
|
||||
return trimQuotes(txt)
|
||||
case ctx.NUMBER() != nil:
|
||||
number, err := strconv.ParseFloat(ctx.NUMBER().GetText(), 64)
|
||||
if err != nil {
|
||||
v.errors = append(v.errors, fmt.Sprintf("failed to parse number %s", ctx.NUMBER().GetText()))
|
||||
return nil
|
||||
}
|
||||
return number
|
||||
case ctx.BOOL() != nil:
|
||||
boolText := strings.ToLower(ctx.BOOL().GetText())
|
||||
return boolText == "true"
|
||||
case ctx.KEY() != nil:
|
||||
return ctx.KEY().GetText()
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
355
pkg/querybuilder/filter_expr_parse_test.go
Normal file
355
pkg/querybuilder/filter_expr_parse_test.go
Normal file
@@ -0,0 +1,355 @@
|
||||
package querybuilder
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
)
|
||||
|
||||
// helper to build a simple key with unspecified context/datatype.
|
||||
func fk(_ *testing.T, name string) *telemetrytypes.TelemetryFieldKey {
|
||||
key := telemetrytypes.GetFieldKeyFromKeyText(name)
|
||||
return &key
|
||||
}
|
||||
|
||||
func TestParseFilterExpr_NestedConditions(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
expr string
|
||||
want *qbtypes.FilterExprNode
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "empty expression returns nil",
|
||||
expr: " ",
|
||||
want: qbtypes.NewEmptyFilterExprNode(),
|
||||
},
|
||||
{
|
||||
name: "invalid expression returns error",
|
||||
expr: "attributes.key =",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "single simple equality leaf",
|
||||
expr: "attributes.host.name = 'frontend'",
|
||||
want: &qbtypes.FilterExprNode{
|
||||
Op: qbtypes.LogicalOpLeaf,
|
||||
Conditions: []qbtypes.FilterCondition{
|
||||
{
|
||||
Key: fk(t, "attributes.host.name"),
|
||||
Op: qbtypes.FilterOperatorEqual,
|
||||
Value: "frontend",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "AND with BETWEEN numeric range and NOT boolean comparison",
|
||||
expr: "attributes.status_code BETWEEN 500 AND 599 AND NOT attributes.is_error = true",
|
||||
want: &qbtypes.FilterExprNode{
|
||||
Op: qbtypes.LogicalOpAnd,
|
||||
Children: []*qbtypes.FilterExprNode{
|
||||
{
|
||||
Op: qbtypes.LogicalOpLeaf,
|
||||
Conditions: []qbtypes.FilterCondition{
|
||||
{
|
||||
Key: fk(t, "attributes.status_code"),
|
||||
Op: qbtypes.FilterOperatorBetween,
|
||||
Value: []any{
|
||||
float64(500),
|
||||
float64(599),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Negated: true,
|
||||
Op: qbtypes.LogicalOpLeaf,
|
||||
Conditions: []qbtypes.FilterCondition{
|
||||
{
|
||||
Key: fk(t, "attributes.is_error"),
|
||||
Op: qbtypes.FilterOperatorEqual,
|
||||
Value: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "OR with IN list and NOT IN list using different syntaxes",
|
||||
expr: "attributes.service.name IN ('api','worker') OR resource.region NOT IN [\"us-east-1\",\"us-west-2\"]",
|
||||
want: &qbtypes.FilterExprNode{
|
||||
Op: qbtypes.LogicalOpOr,
|
||||
Children: []*qbtypes.FilterExprNode{
|
||||
{
|
||||
Op: qbtypes.LogicalOpLeaf,
|
||||
Conditions: []qbtypes.FilterCondition{
|
||||
{
|
||||
Key: fk(t, "attributes.service.name"),
|
||||
Op: qbtypes.FilterOperatorIn,
|
||||
Value: []any{
|
||||
"api",
|
||||
"worker",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Op: qbtypes.LogicalOpLeaf,
|
||||
Conditions: []qbtypes.FilterCondition{
|
||||
{
|
||||
Key: fk(t, "resource.region"),
|
||||
Op: qbtypes.FilterOperatorNotIn,
|
||||
Value: []any{
|
||||
"us-east-1",
|
||||
"us-west-2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "AND chain of string pattern operators with NOT variants",
|
||||
expr: "attributes.message LIKE 'error%' AND attributes.message NOT ILIKE '%debug%' AND attributes.message REGEXP 'err[0-9]+' AND attributes.message NOT CONTAINS 'trace'",
|
||||
want: &qbtypes.FilterExprNode{
|
||||
Op: qbtypes.LogicalOpAnd,
|
||||
Children: []*qbtypes.FilterExprNode{
|
||||
{
|
||||
Op: qbtypes.LogicalOpLeaf,
|
||||
Conditions: []qbtypes.FilterCondition{
|
||||
{
|
||||
Key: fk(t, "attributes.message"),
|
||||
Op: qbtypes.FilterOperatorLike,
|
||||
Value: "error%",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Op: qbtypes.LogicalOpLeaf,
|
||||
Conditions: []qbtypes.FilterCondition{
|
||||
{
|
||||
Key: fk(t, "attributes.message"),
|
||||
Op: qbtypes.FilterOperatorNotILike,
|
||||
Value: "%debug%",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Op: qbtypes.LogicalOpLeaf,
|
||||
Conditions: []qbtypes.FilterCondition{
|
||||
{
|
||||
Key: fk(t, "attributes.message"),
|
||||
Op: qbtypes.FilterOperatorRegexp,
|
||||
Value: "err[0-9]+",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Op: qbtypes.LogicalOpLeaf,
|
||||
Conditions: []qbtypes.FilterCondition{
|
||||
{
|
||||
Key: fk(t, "attributes.message"),
|
||||
Op: qbtypes.FilterOperatorNotContains,
|
||||
Value: "trace",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "EXISTS and NOT EXISTS in OR expression",
|
||||
expr: "attributes.host EXISTS OR attributes.cluster NOT EXISTS",
|
||||
want: &qbtypes.FilterExprNode{
|
||||
Op: qbtypes.LogicalOpOr,
|
||||
Children: []*qbtypes.FilterExprNode{
|
||||
{
|
||||
Op: qbtypes.LogicalOpLeaf,
|
||||
Conditions: []qbtypes.FilterCondition{
|
||||
{
|
||||
Key: fk(t, "attributes.host"),
|
||||
Op: qbtypes.FilterOperatorExists,
|
||||
Value: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Op: qbtypes.LogicalOpLeaf,
|
||||
Conditions: []qbtypes.FilterCondition{
|
||||
{
|
||||
Key: fk(t, "attributes.cluster"),
|
||||
Op: qbtypes.FilterOperatorNotExists,
|
||||
Value: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "KEY used as value in equality",
|
||||
expr: "attributes.left = other_key",
|
||||
want: &qbtypes.FilterExprNode{
|
||||
Op: qbtypes.LogicalOpLeaf,
|
||||
Conditions: []qbtypes.FilterCondition{
|
||||
{
|
||||
Key: fk(t, "attributes.left"),
|
||||
Op: qbtypes.FilterOperatorEqual,
|
||||
Value: "other_key",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nested OR inside AND with NOT on inner group",
|
||||
// NOT applies to the whole parenthesized OR group.
|
||||
expr: "attributes.env = 'prod' AND NOT (attributes.team = 'core' OR attributes.team = 'platform')",
|
||||
want: &qbtypes.FilterExprNode{
|
||||
Op: qbtypes.LogicalOpAnd,
|
||||
Children: []*qbtypes.FilterExprNode{
|
||||
{
|
||||
Op: qbtypes.LogicalOpLeaf,
|
||||
Conditions: []qbtypes.FilterCondition{
|
||||
{
|
||||
Key: fk(t, "attributes.env"),
|
||||
Op: qbtypes.FilterOperatorEqual,
|
||||
Value: "prod",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Negated: true,
|
||||
Op: qbtypes.LogicalOpOr,
|
||||
Children: []*qbtypes.FilterExprNode{
|
||||
{
|
||||
Op: qbtypes.LogicalOpLeaf,
|
||||
Conditions: []qbtypes.FilterCondition{
|
||||
{
|
||||
Key: fk(t, "attributes.team"),
|
||||
Op: qbtypes.FilterOperatorEqual,
|
||||
Value: "core",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Op: qbtypes.LogicalOpLeaf,
|
||||
Conditions: []qbtypes.FilterCondition{
|
||||
{
|
||||
Key: fk(t, "attributes.team"),
|
||||
Op: qbtypes.FilterOperatorEqual,
|
||||
Value: "platform",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple nesting levels mixing AND/OR",
|
||||
expr: "(attributes.status = 'critical' OR attributes.status = 'warning') AND (resource.region = 'us-east-1' OR (resource.region = 'us-west-2' AND attributes.tier = 'backend'))",
|
||||
want: &qbtypes.FilterExprNode{
|
||||
Op: qbtypes.LogicalOpAnd,
|
||||
Children: []*qbtypes.FilterExprNode{
|
||||
{
|
||||
Op: qbtypes.LogicalOpOr,
|
||||
Children: []*qbtypes.FilterExprNode{
|
||||
{
|
||||
Op: qbtypes.LogicalOpLeaf,
|
||||
Conditions: []qbtypes.FilterCondition{
|
||||
{
|
||||
Key: fk(t, "attributes.status"),
|
||||
Op: qbtypes.FilterOperatorEqual,
|
||||
Value: "critical",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Op: qbtypes.LogicalOpLeaf,
|
||||
Conditions: []qbtypes.FilterCondition{
|
||||
{
|
||||
Key: fk(t, "attributes.status"),
|
||||
Op: qbtypes.FilterOperatorEqual,
|
||||
Value: "warning",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Op: qbtypes.LogicalOpOr,
|
||||
Children: []*qbtypes.FilterExprNode{
|
||||
{
|
||||
Op: qbtypes.LogicalOpLeaf,
|
||||
Conditions: []qbtypes.FilterCondition{
|
||||
{
|
||||
Key: fk(t, "resource.region"),
|
||||
Op: qbtypes.FilterOperatorEqual,
|
||||
Value: "us-east-1",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Op: qbtypes.LogicalOpAnd,
|
||||
Children: []*qbtypes.FilterExprNode{
|
||||
{
|
||||
Op: qbtypes.LogicalOpLeaf,
|
||||
Conditions: []qbtypes.FilterCondition{
|
||||
{
|
||||
Key: fk(t, "resource.region"),
|
||||
Op: qbtypes.FilterOperatorEqual,
|
||||
Value: "us-west-2",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Op: qbtypes.LogicalOpLeaf,
|
||||
Conditions: []qbtypes.FilterCondition{
|
||||
{
|
||||
Key: fk(t, "attributes.tier"),
|
||||
Op: qbtypes.FilterOperatorEqual,
|
||||
Value: "backend",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "random test",
|
||||
expr: "attributes.status, attributes.status_code = 200",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := ParseFilterExpr(tt.expr)
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Fatalf("expected error, got nil")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Fatalf("unexpected tree for expr %q\n got: %#v\n want: %#v", tt.expr, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -169,6 +169,7 @@ func NewSQLMigrationProviderFactories(
|
||||
sqlmigration.NewAddAnonymousPublicDashboardTransactionFactory(sqlstore),
|
||||
sqlmigration.NewAddRootUserFactory(sqlstore, sqlschema),
|
||||
sqlmigration.NewAddUserEmailOrgIDIndexFactory(sqlstore, sqlschema),
|
||||
sqlmigration.NewMigratePipelineFiltersV5Factory(sqlstore),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
135
pkg/sqlmigration/066_migrate_pipeline_filters_v5.go
Normal file
135
pkg/sqlmigration/066_migrate_pipeline_filters_v5.go
Normal file
@@ -0,0 +1,135 @@
|
||||
package sqlmigration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/queryBuilderToExpr"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/transition"
|
||||
"github.com/SigNoz/signoz/pkg/types/pipelinetypes"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/migrate"
|
||||
)
|
||||
|
||||
type migratePipelineFiltersV5 struct {
|
||||
store sqlstore.SQLStore
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
func NewMigratePipelineFiltersV5Factory(
|
||||
store sqlstore.SQLStore,
|
||||
) factory.ProviderFactory[SQLMigration, Config] {
|
||||
return factory.NewProviderFactory(
|
||||
factory.MustNewName("migrate_pipeline_filters_v5"),
|
||||
func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
|
||||
return newMigratePipelineFiltersV5(ctx, c, store, ps.Logger)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func newMigratePipelineFiltersV5(
|
||||
_ context.Context,
|
||||
_ Config,
|
||||
store sqlstore.SQLStore,
|
||||
logger *slog.Logger,
|
||||
) (SQLMigration, error) {
|
||||
if logger == nil {
|
||||
logger = slog.Default()
|
||||
}
|
||||
|
||||
return &migratePipelineFiltersV5{
|
||||
store: store,
|
||||
logger: logger,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (migration *migratePipelineFiltersV5) Register(migrations *migrate.Migrations) error {
|
||||
if err := migrations.Register(migration.Up, migration.Down); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (migration *migratePipelineFiltersV5) Up(ctx context.Context, db *bun.DB) error {
|
||||
tx, err := db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = tx.Rollback()
|
||||
}()
|
||||
|
||||
var pipelines []pipelinetypes.StoreablePipeline
|
||||
if err := tx.NewSelect().
|
||||
Table("pipelines").
|
||||
Column("id", "filter").
|
||||
Scan(ctx, &pipelines); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, p := range pipelines {
|
||||
raw := strings.TrimSpace(p.FilterString)
|
||||
if raw == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip migration if this already looks like a v5 filter
|
||||
// ({ "expression": "..." }), but still persist the normalized form
|
||||
// if we changed it.
|
||||
var vf qbtypes.Filter
|
||||
if err := json.Unmarshal([]byte(raw), &vf); err == nil && strings.TrimSpace(vf.Expression) != "" {
|
||||
continue
|
||||
}
|
||||
|
||||
var filterSet v3.FilterSet
|
||||
if err := json.Unmarshal([]byte(raw), &filterSet); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Attempt to treat the existing JSON as a v3 FilterSet and build a v5
|
||||
// expression from it, using the normalized payload.
|
||||
expr, migrated, err := transition.BuildFilterExpressionFromFilterSet(ctx, migration.logger, "logs", &filterSet)
|
||||
if err != nil || !migrated || strings.TrimSpace(expr) == "" {
|
||||
return err
|
||||
}
|
||||
|
||||
filter := &qbtypes.Filter{Expression: expr}
|
||||
_, err = queryBuilderToExpr.Parse(filter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Store back as v5-style Filter JSON.
|
||||
out, err := json.Marshal(filter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := tx.NewUpdate().
|
||||
Table("pipelines").
|
||||
Set("filter = ?", string(out)).
|
||||
Where("id = ?", p.ID).
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (migration *migratePipelineFiltersV5) Down(ctx context.Context, db *bun.DB) error {
|
||||
// Not reversible: v3 FilterSet structure is lost once we convert to a v5 expression.
|
||||
return nil
|
||||
}
|
||||
@@ -61,11 +61,13 @@ var (
|
||||
}
|
||||
)
|
||||
|
||||
type fieldMapper struct {}
|
||||
type fieldMapper struct {
|
||||
}
|
||||
|
||||
func NewFieldMapper() qbtypes.FieldMapper {
|
||||
return &fieldMapper{}
|
||||
}
|
||||
|
||||
func (m *fieldMapper) getColumn(_ context.Context, key *telemetrytypes.TelemetryFieldKey) (*schema.Column, error) {
|
||||
switch key.FieldContext {
|
||||
case telemetrytypes.FieldContextResource:
|
||||
@@ -252,27 +254,12 @@ func (m *fieldMapper) buildFieldForJSON(key *telemetrytypes.TelemetryFieldKey) (
|
||||
"plan length is less than 2 for promoted path: %s", key.Name)
|
||||
}
|
||||
|
||||
node := plan[1]
|
||||
promotedExpr := fmt.Sprintf(
|
||||
"dynamicElement(%s, '%s')",
|
||||
node.FieldPath(),
|
||||
node.TerminalConfig.ElemType.StringValue(),
|
||||
// promoted column first then body_json column
|
||||
// TODO(Piyush): Change this in future for better performance
|
||||
expr = fmt.Sprintf("coalesce(%s, %s)",
|
||||
fmt.Sprintf("dynamicElement(%s, '%s')", plan[1].FieldPath(), plan[1].TerminalConfig.ElemType.StringValue()),
|
||||
expr,
|
||||
)
|
||||
|
||||
// dynamicElement returns NULL for scalar types or an empty array for array types.
|
||||
if node.TerminalConfig.ElemType.IsArray {
|
||||
expr = fmt.Sprintf(
|
||||
"if(length(%s) > 0, %s, %s)",
|
||||
promotedExpr,
|
||||
promotedExpr,
|
||||
expr,
|
||||
)
|
||||
} else {
|
||||
// promoted column first then body_json column
|
||||
// TODO(Piyush): Change this in future for better performance
|
||||
expr = fmt.Sprintf("coalesce(%s, %s)", promotedExpr, expr)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return expr, nil
|
||||
@@ -294,7 +281,8 @@ func (m *fieldMapper) buildArrayConcat(plan telemetrytypes.JSONAccessPlan) (stri
|
||||
}
|
||||
|
||||
// Build arrayMap expressions for ALL available branches at the root level.
|
||||
// Iterate branches in deterministic order (JSON then Dynamic)
|
||||
// Iterate branches in deterministic order (JSON then Dynamic) so generated SQL
|
||||
// is stable across environments; map iteration order is random in Go.
|
||||
var arrayMapExpressions []string
|
||||
for _, node := range plan {
|
||||
for _, branchType := range node.BranchesInOrder() {
|
||||
|
||||
@@ -32,6 +32,7 @@ func NewJSONConditionBuilder(key *telemetrytypes.TelemetryFieldKey, valueType te
|
||||
|
||||
// BuildCondition builds the full WHERE condition for body_json JSON paths
|
||||
func (c *jsonConditionBuilder) buildJSONCondition(operator qbtypes.FilterOperator, value any, sb *sqlbuilder.SelectBuilder) (string, error) {
|
||||
|
||||
conditions := []string{}
|
||||
for _, node := range c.key.JSONPlan {
|
||||
condition, err := c.emitPlannedCondition(node, operator, value, sb)
|
||||
@@ -72,9 +73,9 @@ func (c *jsonConditionBuilder) buildTerminalCondition(node *telemetrytypes.JSONA
|
||||
|
||||
// switch operator for array membership checks
|
||||
switch operator {
|
||||
case qbtypes.FilterOperatorContains:
|
||||
case qbtypes.FilterOperatorContains, qbtypes.FilterOperatorIn:
|
||||
operator = qbtypes.FilterOperatorEqual
|
||||
case qbtypes.FilterOperatorNotContains:
|
||||
case qbtypes.FilterOperatorNotContains, qbtypes.FilterOperatorNotIn:
|
||||
operator = qbtypes.FilterOperatorNotEqual
|
||||
}
|
||||
}
|
||||
@@ -190,14 +191,13 @@ func (c *jsonConditionBuilder) buildArrayMembershipCondition(node *telemetrytype
|
||||
arrayExpr = typedArrayExpr()
|
||||
}
|
||||
|
||||
key := "x"
|
||||
fieldExpr, value := querybuilder.DataTypeCollisionHandledFieldName(&localKeyCopy, value, key, operator)
|
||||
fieldExpr, value := querybuilder.DataTypeCollisionHandledFieldName(&localKeyCopy, value, "x", operator)
|
||||
op, err := c.applyOperator(sb, fieldExpr, operator, value)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return fmt.Sprintf("arrayExists(%s -> %s, %s)", key, op, arrayExpr), nil
|
||||
return fmt.Sprintf("arrayExists(%s -> %s, %s)", fieldExpr, op, arrayExpr), nil
|
||||
}
|
||||
|
||||
// recurseArrayHops recursively builds array traversal conditions
|
||||
@@ -279,31 +279,27 @@ func (c *jsonConditionBuilder) applyOperator(sb *sqlbuilder.SelectBuilder, field
|
||||
case qbtypes.FilterOperatorNotContains:
|
||||
return sb.NotILike(fieldExpr, fmt.Sprintf("%%%v%%", value)), nil
|
||||
case qbtypes.FilterOperatorIn, qbtypes.FilterOperatorNotIn:
|
||||
// emulate IN/NOT IN using OR/AND over equals to leverage indexes consistently
|
||||
values, ok := value.([]any)
|
||||
if !ok {
|
||||
values = []any{value}
|
||||
}
|
||||
if operator == qbtypes.FilterOperatorIn {
|
||||
return sb.In(fieldExpr, values...), nil
|
||||
conds := []string{}
|
||||
for _, v := range values {
|
||||
if operator == qbtypes.FilterOperatorIn {
|
||||
conds = append(conds, sb.E(fieldExpr, v))
|
||||
} else {
|
||||
conds = append(conds, sb.NE(fieldExpr, v))
|
||||
}
|
||||
}
|
||||
return sb.NotIn(fieldExpr, values...), nil
|
||||
if operator == qbtypes.FilterOperatorIn {
|
||||
return sb.Or(conds...), nil
|
||||
}
|
||||
return sb.And(conds...), nil
|
||||
case qbtypes.FilterOperatorExists:
|
||||
return fmt.Sprintf("%s IS NOT NULL", fieldExpr), nil
|
||||
case qbtypes.FilterOperatorNotExists:
|
||||
return fmt.Sprintf("%s IS NULL", fieldExpr), nil
|
||||
// between and not between
|
||||
case qbtypes.FilterOperatorBetween, qbtypes.FilterOperatorNotBetween:
|
||||
values, ok := value.([]any)
|
||||
if !ok {
|
||||
return "", qbtypes.ErrBetweenValues
|
||||
}
|
||||
if len(values) != 2 {
|
||||
return "", qbtypes.ErrBetweenValues
|
||||
}
|
||||
if operator == qbtypes.FilterOperatorBetween {
|
||||
return sb.Between(fieldExpr, values[0], values[1]), nil
|
||||
}
|
||||
return sb.NotBetween(fieldExpr, values[0], values[1]), nil
|
||||
default:
|
||||
return "", qbtypes.ErrUnsupportedOperator
|
||||
}
|
||||
|
||||
@@ -316,7 +316,7 @@ func TestStatementBuilderListQueryBody(t *testing.T) {
|
||||
Limit: 10,
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, body_json, body_json_promoted, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((arrayExists(`body_json.education`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), dynamicElement(`body_json.education`.`parameters`, 'Array(Nullable(Float64))')) OR arrayExists(x -> toFloat64(x) = ?, dynamicElement(`body_json.education`.`parameters`, 'Array(Nullable(Float64))'))), dynamicElement(body_json.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) OR (arrayExists(`body_json.education`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), arrayMap(x->dynamicElement(x, 'Float64'), arrayFilter(x->(dynamicType(x) = 'Float64'), dynamicElement(`body_json.education`.`parameters`, 'Array(Dynamic)')))) OR arrayExists(x -> toFloat64(x) = ?, arrayMap(x->dynamicElement(x, 'Float64'), arrayFilter(x->(dynamicType(x) = 'Float64'), dynamicElement(`body_json.education`.`parameters`, 'Array(Dynamic)'))))), dynamicElement(body_json.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, body_json, body_json_promoted, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((arrayExists(`body_json.education`-> (arrayExists(toString(x) -> LOWER(toString(x)) LIKE LOWER(?), dynamicElement(`body_json.education`.`parameters`, 'Array(Nullable(Float64))')) OR arrayExists(toFloat64(x) -> toFloat64(x) = ?, dynamicElement(`body_json.education`.`parameters`, 'Array(Nullable(Float64))'))), dynamicElement(body_json.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) OR (arrayExists(`body_json.education`-> (arrayExists(toString(x) -> LOWER(toString(x)) LIKE LOWER(?), arrayMap(x->dynamicElement(x, 'Float64'), arrayFilter(x->(dynamicType(x) = 'Float64'), dynamicElement(`body_json.education`.`parameters`, 'Array(Dynamic)')))) OR arrayExists(toFloat64(x) -> toFloat64(x) = ?, arrayMap(x->dynamicElement(x, 'Float64'), arrayFilter(x->(dynamicType(x) = 'Float64'), dynamicElement(`body_json.education`.`parameters`, 'Array(Dynamic)'))))), dynamicElement(body_json.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Args: []any{uint64(1747945619), uint64(1747983448), "%1.65%", 1.65, "%1.65%", 1.65, "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
|
||||
Warnings: []string{"Key `education[].parameters` is ambiguous, found 2 different combinations of field context / data type: [name=education[].parameters,context=body,datatype=[]float64,jsondatatype=Array(Nullable(Float64)) name=education[].parameters,context=body,datatype=[]dynamic,jsondatatype=Array(Dynamic)]."},
|
||||
},
|
||||
@@ -345,7 +345,7 @@ func TestStatementBuilderListQueryBody(t *testing.T) {
|
||||
Limit: 10,
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, body_json, body_json_promoted, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((arrayExists(`body_json.education`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), dynamicElement(`body_json.education`.`parameters`, 'Array(Nullable(Float64))')) OR arrayExists(x -> x = ?, dynamicElement(`body_json.education`.`parameters`, 'Array(Nullable(Float64))'))), dynamicElement(body_json.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) OR (arrayExists(`body_json.education`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), arrayMap(x->dynamicElement(x, 'Bool'), arrayFilter(x->(dynamicType(x) = 'Bool'), dynamicElement(`body_json.education`.`parameters`, 'Array(Dynamic)')))) OR arrayExists(x -> x = ?, arrayMap(x->dynamicElement(x, 'Bool'), arrayFilter(x->(dynamicType(x) = 'Bool'), dynamicElement(`body_json.education`.`parameters`, 'Array(Dynamic)'))))), dynamicElement(body_json.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, body_json, body_json_promoted, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((arrayExists(`body_json.education`-> (arrayExists(toString(x) -> LOWER(toString(x)) LIKE LOWER(?), dynamicElement(`body_json.education`.`parameters`, 'Array(Nullable(Float64))')) OR arrayExists(x -> x = ?, dynamicElement(`body_json.education`.`parameters`, 'Array(Nullable(Float64))'))), dynamicElement(body_json.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) OR (arrayExists(`body_json.education`-> (arrayExists(toString(x) -> LOWER(toString(x)) LIKE LOWER(?), arrayMap(x->dynamicElement(x, 'Bool'), arrayFilter(x->(dynamicType(x) = 'Bool'), dynamicElement(`body_json.education`.`parameters`, 'Array(Dynamic)')))) OR arrayExists(x -> x = ?, arrayMap(x->dynamicElement(x, 'Bool'), arrayFilter(x->(dynamicType(x) = 'Bool'), dynamicElement(`body_json.education`.`parameters`, 'Array(Dynamic)'))))), dynamicElement(body_json.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Args: []any{uint64(1747945619), uint64(1747983448), "%true%", true, "%true%", true, "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
|
||||
Warnings: []string{"Key `education[].parameters` is ambiguous, found 2 different combinations of field context / data type: [name=education[].parameters,context=body,datatype=[]float64,jsondatatype=Array(Nullable(Float64)) name=education[].parameters,context=body,datatype=[]dynamic,jsondatatype=Array(Dynamic)]."},
|
||||
},
|
||||
@@ -360,55 +360,12 @@ func TestStatementBuilderListQueryBody(t *testing.T) {
|
||||
Limit: 10,
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, body_json, body_json_promoted, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((arrayExists(`body_json.education`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), dynamicElement(`body_json.education`.`parameters`, 'Array(Nullable(Float64))')) OR arrayExists(x -> toString(x) = ?, dynamicElement(`body_json.education`.`parameters`, 'Array(Nullable(Float64))'))), dynamicElement(body_json.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) OR (arrayExists(`body_json.education`-> (arrayExists(x -> LOWER(x) LIKE LOWER(?), arrayMap(x->dynamicElement(x, 'String'), arrayFilter(x->(dynamicType(x) = 'String'), dynamicElement(`body_json.education`.`parameters`, 'Array(Dynamic)')))) OR arrayExists(x -> x = ?, arrayMap(x->dynamicElement(x, 'String'), arrayFilter(x->(dynamicType(x) = 'String'), dynamicElement(`body_json.education`.`parameters`, 'Array(Dynamic)'))))), dynamicElement(body_json.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, body_json, body_json_promoted, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((arrayExists(`body_json.education`-> (arrayExists(toString(x) -> LOWER(toString(x)) LIKE LOWER(?), dynamicElement(`body_json.education`.`parameters`, 'Array(Nullable(Float64))')) OR arrayExists(toString(x) -> toString(x) = ?, dynamicElement(`body_json.education`.`parameters`, 'Array(Nullable(Float64))'))), dynamicElement(body_json.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) OR (arrayExists(`body_json.education`-> (arrayExists(x -> LOWER(x) LIKE LOWER(?), arrayMap(x->dynamicElement(x, 'String'), arrayFilter(x->(dynamicType(x) = 'String'), dynamicElement(`body_json.education`.`parameters`, 'Array(Dynamic)')))) OR arrayExists(x -> x = ?, arrayMap(x->dynamicElement(x, 'String'), arrayFilter(x->(dynamicType(x) = 'String'), dynamicElement(`body_json.education`.`parameters`, 'Array(Dynamic)'))))), dynamicElement(body_json.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Args: []any{uint64(1747945619), uint64(1747983448), "%passed%", "passed", "%passed%", "passed", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
|
||||
Warnings: []string{"Key `education[].parameters` is ambiguous, found 2 different combinations of field context / data type: [name=education[].parameters,context=body,datatype=[]float64,jsondatatype=Array(Nullable(Float64)) name=education[].parameters,context=body,datatype=[]dynamic,jsondatatype=Array(Dynamic)]."},
|
||||
},
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
name: "Dynamic array IN Operator",
|
||||
requestType: qbtypes.RequestTypeRaw,
|
||||
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
|
||||
Signal: telemetrytypes.SignalLogs,
|
||||
Filter: &qbtypes.Filter{Expression: "body.education[].parameters IN [1.65, 1.99]"},
|
||||
Limit: 10,
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, body_json, body_json_promoted, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((arrayExists(`body_json.education`-> (arrayExists(x -> toFloat64(x) IN (?, ?), dynamicElement(`body_json.education`.`parameters`, 'Array(Nullable(Float64))'))), dynamicElement(body_json.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) OR (arrayExists(`body_json.education`-> (arrayExists(x -> x IN (?, ?), arrayMap(x->dynamicElement(x, 'Array(Nullable(Float64))'), arrayFilter(x->(dynamicType(x) = 'Array(Nullable(Float64))'), dynamicElement(`body_json.education`.`parameters`, 'Array(Dynamic)'))))), dynamicElement(body_json.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Args: []any{uint64(1747945619), uint64(1747983448), 1.65, 1.99, 1.65, 1.99, "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
|
||||
Warnings: []string{"Key `education[].parameters` is ambiguous, found 2 different combinations of field context / data type: [name=education[].parameters,context=body,datatype=[]float64,jsondatatype=Array(Nullable(Float64)) name=education[].parameters,context=body,datatype=[]dynamic,jsondatatype=Array(Dynamic)]."},
|
||||
},
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
name: "Integer BETWEEN Operator",
|
||||
requestType: qbtypes.RequestTypeRaw,
|
||||
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
|
||||
Signal: telemetrytypes.SignalLogs,
|
||||
Filter: &qbtypes.Filter{Expression: "education[].awards[].semester BETWEEN 2 AND 4"},
|
||||
Limit: 10,
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE (arrayExists(`body_json.education`-> (arrayExists(`body_json.education[].awards`-> toFloat64(dynamicElement(`body_json.education[].awards`.`semester`, 'Int64')) BETWEEN ? AND ?, dynamicElement(`body_json.education`.`awards`, 'Array(JSON(max_dynamic_types=8, max_dynamic_paths=0))')) OR arrayExists(`body_json.education[].awards`-> toFloat64(dynamicElement(`body_json.education[].awards`.`semester`, 'Int64')) BETWEEN ? AND ?, arrayMap(x->dynamicElement(x, 'JSON'), arrayFilter(x->(dynamicType(x) = 'JSON'), dynamicElement(`body_json.education`.`awards`, 'Array(Dynamic)'))))), dynamicElement(body_json.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, body_json, body_json_promoted, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND (arrayExists(`body_json.education`-> (arrayExists(`body_json.education[].awards`-> toFloat64(dynamicElement(`body_json.education[].awards`.`semester`, 'Int64')) BETWEEN ? AND ?, dynamicElement(`body_json.education`.`awards`, 'Array(JSON(max_dynamic_types=8, max_dynamic_paths=0))')) OR arrayExists(`body_json.education[].awards`-> toFloat64(dynamicElement(`body_json.education[].awards`.`semester`, 'Int64')) BETWEEN ? AND ?, arrayMap(x->dynamicElement(x, 'JSON'), arrayFilter(x->(dynamicType(x) = 'JSON'), dynamicElement(`body_json.education`.`awards`, 'Array(Dynamic)'))))), dynamicElement(body_json.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Args: []any{float64(2), float64(4), float64(2), float64(4), uint64(1747945619), uint64(1747983448), float64(2), float64(4), float64(2), float64(4), "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
|
||||
},
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
name: "Integer IN Operator",
|
||||
requestType: qbtypes.RequestTypeRaw,
|
||||
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
|
||||
Signal: telemetrytypes.SignalLogs,
|
||||
Filter: &qbtypes.Filter{Expression: "education[].awards[].semester IN [2, 4]"},
|
||||
Limit: 10,
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE (arrayExists(`body_json.education`-> (arrayExists(`body_json.education[].awards`-> toFloat64(dynamicElement(`body_json.education[].awards`.`semester`, 'Int64')) IN (?, ?), dynamicElement(`body_json.education`.`awards`, 'Array(JSON(max_dynamic_types=8, max_dynamic_paths=0))')) OR arrayExists(`body_json.education[].awards`-> toFloat64(dynamicElement(`body_json.education[].awards`.`semester`, 'Int64')) IN (?, ?), arrayMap(x->dynamicElement(x, 'JSON'), arrayFilter(x->(dynamicType(x) = 'JSON'), dynamicElement(`body_json.education`.`awards`, 'Array(Dynamic)'))))), dynamicElement(body_json.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, body_json, body_json_promoted, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND (arrayExists(`body_json.education`-> (arrayExists(`body_json.education[].awards`-> toFloat64(dynamicElement(`body_json.education[].awards`.`semester`, 'Int64')) IN (?, ?), dynamicElement(`body_json.education`.`awards`, 'Array(JSON(max_dynamic_types=8, max_dynamic_paths=0))')) OR arrayExists(`body_json.education[].awards`-> toFloat64(dynamicElement(`body_json.education[].awards`.`semester`, 'Int64')) IN (?, ?), arrayMap(x->dynamicElement(x, 'JSON'), arrayFilter(x->(dynamicType(x) = 'JSON'), dynamicElement(`body_json.education`.`awards`, 'Array(Dynamic)'))))), dynamicElement(body_json.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Args: []any{float64(2), float64(4), float64(2), float64(4), uint64(1747945619), uint64(1747983448), float64(2), float64(4), float64(2), float64(4), "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
|
||||
},
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
name: "Equals to 'sports' inside array of awards",
|
||||
requestType: qbtypes.RequestTypeRaw,
|
||||
@@ -432,7 +389,7 @@ func TestStatementBuilderListQueryBody(t *testing.T) {
|
||||
Limit: 10,
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, body_json, body_json_promoted, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((arrayExists(`body_json.interests`-> arrayExists(`body_json.interests[].entities`-> arrayExists(`body_json.interests[].entities[].reviews`-> arrayExists(`body_json.interests[].entities[].reviews[].entries`-> arrayExists(`body_json.interests[].entities[].reviews[].entries[].metadata`-> arrayExists(`body_json.interests[].entities[].reviews[].entries[].metadata[].positions`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), dynamicElement(`body_json.interests[].entities[].reviews[].entries[].metadata[].positions`.`ratings`, 'Array(Nullable(Int64))')) OR arrayExists(x -> toFloat64(x) = ?, dynamicElement(`body_json.interests[].entities[].reviews[].entries[].metadata[].positions`.`ratings`, 'Array(Nullable(Int64))'))), dynamicElement(`body_json.interests[].entities[].reviews[].entries[].metadata`.`positions`, 'Array(JSON(max_dynamic_types=0, max_dynamic_paths=0))')), dynamicElement(`body_json.interests[].entities[].reviews[].entries`.`metadata`, 'Array(JSON(max_dynamic_types=1, max_dynamic_paths=0))')), dynamicElement(`body_json.interests[].entities[].reviews`.`entries`, 'Array(JSON(max_dynamic_types=2, max_dynamic_paths=0))')), dynamicElement(`body_json.interests[].entities`.`reviews`, 'Array(JSON(max_dynamic_types=4, max_dynamic_paths=0))')), dynamicElement(`body_json.interests`.`entities`, 'Array(JSON(max_dynamic_types=8, max_dynamic_paths=0))')), dynamicElement(body_json.`interests`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) OR (arrayExists(`body_json.interests`-> arrayExists(`body_json.interests[].entities`-> arrayExists(`body_json.interests[].entities[].reviews`-> arrayExists(`body_json.interests[].entities[].reviews[].entries`-> arrayExists(`body_json.interests[].entities[].reviews[].entries[].metadata`-> arrayExists(`body_json.interests[].entities[].reviews[].entries[].metadata[].positions`-> (arrayExists(x -> LOWER(x) LIKE LOWER(?), dynamicElement(`body_json.interests[].entities[].reviews[].entries[].metadata[].positions`.`ratings`, 'Array(Nullable(String))')) OR arrayExists(x -> toFloat64OrNull(x) = ?, dynamicElement(`body_json.interests[].entities[].reviews[].entries[].metadata[].positions`.`ratings`, 'Array(Nullable(String))'))), dynamicElement(`body_json.interests[].entities[].reviews[].entries[].metadata`.`positions`, 'Array(JSON(max_dynamic_types=0, max_dynamic_paths=0))')), dynamicElement(`body_json.interests[].entities[].reviews[].entries`.`metadata`, 'Array(JSON(max_dynamic_types=1, max_dynamic_paths=0))')), dynamicElement(`body_json.interests[].entities[].reviews`.`entries`, 'Array(JSON(max_dynamic_types=2, max_dynamic_paths=0))')), dynamicElement(`body_json.interests[].entities`.`reviews`, 'Array(JSON(max_dynamic_types=4, max_dynamic_paths=0))')), dynamicElement(`body_json.interests`.`entities`, 'Array(JSON(max_dynamic_types=8, max_dynamic_paths=0))')), dynamicElement(body_json.`interests`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, body_json, body_json_promoted, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((arrayExists(`body_json.interests`-> arrayExists(`body_json.interests[].entities`-> arrayExists(`body_json.interests[].entities[].reviews`-> arrayExists(`body_json.interests[].entities[].reviews[].entries`-> arrayExists(`body_json.interests[].entities[].reviews[].entries[].metadata`-> arrayExists(`body_json.interests[].entities[].reviews[].entries[].metadata[].positions`-> (arrayExists(toString(x) -> LOWER(toString(x)) LIKE LOWER(?), dynamicElement(`body_json.interests[].entities[].reviews[].entries[].metadata[].positions`.`ratings`, 'Array(Nullable(Int64))')) OR arrayExists(toFloat64(x) -> toFloat64(x) = ?, dynamicElement(`body_json.interests[].entities[].reviews[].entries[].metadata[].positions`.`ratings`, 'Array(Nullable(Int64))'))), dynamicElement(`body_json.interests[].entities[].reviews[].entries[].metadata`.`positions`, 'Array(JSON(max_dynamic_types=0, max_dynamic_paths=0))')), dynamicElement(`body_json.interests[].entities[].reviews[].entries`.`metadata`, 'Array(JSON(max_dynamic_types=1, max_dynamic_paths=0))')), dynamicElement(`body_json.interests[].entities[].reviews`.`entries`, 'Array(JSON(max_dynamic_types=2, max_dynamic_paths=0))')), dynamicElement(`body_json.interests[].entities`.`reviews`, 'Array(JSON(max_dynamic_types=4, max_dynamic_paths=0))')), dynamicElement(`body_json.interests`.`entities`, 'Array(JSON(max_dynamic_types=8, max_dynamic_paths=0))')), dynamicElement(body_json.`interests`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) OR (arrayExists(`body_json.interests`-> arrayExists(`body_json.interests[].entities`-> arrayExists(`body_json.interests[].entities[].reviews`-> arrayExists(`body_json.interests[].entities[].reviews[].entries`-> arrayExists(`body_json.interests[].entities[].reviews[].entries[].metadata`-> arrayExists(`body_json.interests[].entities[].reviews[].entries[].metadata[].positions`-> (arrayExists(x -> LOWER(x) LIKE LOWER(?), dynamicElement(`body_json.interests[].entities[].reviews[].entries[].metadata[].positions`.`ratings`, 'Array(Nullable(String))')) OR arrayExists(toFloat64OrNull(x) -> toFloat64OrNull(x) = ?, dynamicElement(`body_json.interests[].entities[].reviews[].entries[].metadata[].positions`.`ratings`, 'Array(Nullable(String))'))), dynamicElement(`body_json.interests[].entities[].reviews[].entries[].metadata`.`positions`, 'Array(JSON(max_dynamic_types=0, max_dynamic_paths=0))')), dynamicElement(`body_json.interests[].entities[].reviews[].entries`.`metadata`, 'Array(JSON(max_dynamic_types=1, max_dynamic_paths=0))')), dynamicElement(`body_json.interests[].entities[].reviews`.`entries`, 'Array(JSON(max_dynamic_types=2, max_dynamic_paths=0))')), dynamicElement(`body_json.interests[].entities`.`reviews`, 'Array(JSON(max_dynamic_types=4, max_dynamic_paths=0))')), dynamicElement(`body_json.interests`.`entities`, 'Array(JSON(max_dynamic_types=8, max_dynamic_paths=0))')), dynamicElement(body_json.`interests`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Args: []any{uint64(1747945619), uint64(1747983448), "%4%", float64(4), "%4%", float64(4), "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
|
||||
Warnings: []string{"Key `interests[].entities[].reviews[].entries[].metadata[].positions[].ratings` is ambiguous, found 2 different combinations of field context / data type: [name=interests[].entities[].reviews[].entries[].metadata[].positions[].ratings,context=body,datatype=[]int64,jsondatatype=Array(Nullable(Int64)) name=interests[].entities[].reviews[].entries[].metadata[].positions[].ratings,context=body,datatype=[]string,jsondatatype=Array(Nullable(String))]."},
|
||||
},
|
||||
@@ -447,7 +404,7 @@ func TestStatementBuilderListQueryBody(t *testing.T) {
|
||||
Limit: 10,
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, body_json, body_json_promoted, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((arrayExists(`body_json.interests`-> arrayExists(`body_json.interests[].entities`-> arrayExists(`body_json.interests[].entities[].reviews`-> arrayExists(`body_json.interests[].entities[].reviews[].entries`-> arrayExists(`body_json.interests[].entities[].reviews[].entries[].metadata`-> arrayExists(`body_json.interests[].entities[].reviews[].entries[].metadata[].positions`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), dynamicElement(`body_json.interests[].entities[].reviews[].entries[].metadata[].positions`.`ratings`, 'Array(Nullable(Int64))')) OR arrayExists(x -> toString(x) = ?, dynamicElement(`body_json.interests[].entities[].reviews[].entries[].metadata[].positions`.`ratings`, 'Array(Nullable(Int64))'))), dynamicElement(`body_json.interests[].entities[].reviews[].entries[].metadata`.`positions`, 'Array(JSON(max_dynamic_types=0, max_dynamic_paths=0))')), dynamicElement(`body_json.interests[].entities[].reviews[].entries`.`metadata`, 'Array(JSON(max_dynamic_types=1, max_dynamic_paths=0))')), dynamicElement(`body_json.interests[].entities[].reviews`.`entries`, 'Array(JSON(max_dynamic_types=2, max_dynamic_paths=0))')), dynamicElement(`body_json.interests[].entities`.`reviews`, 'Array(JSON(max_dynamic_types=4, max_dynamic_paths=0))')), dynamicElement(`body_json.interests`.`entities`, 'Array(JSON(max_dynamic_types=8, max_dynamic_paths=0))')), dynamicElement(body_json.`interests`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) OR (arrayExists(`body_json.interests`-> arrayExists(`body_json.interests[].entities`-> arrayExists(`body_json.interests[].entities[].reviews`-> arrayExists(`body_json.interests[].entities[].reviews[].entries`-> arrayExists(`body_json.interests[].entities[].reviews[].entries[].metadata`-> arrayExists(`body_json.interests[].entities[].reviews[].entries[].metadata[].positions`-> (arrayExists(x -> LOWER(x) LIKE LOWER(?), dynamicElement(`body_json.interests[].entities[].reviews[].entries[].metadata[].positions`.`ratings`, 'Array(Nullable(String))')) OR arrayExists(x -> x = ?, dynamicElement(`body_json.interests[].entities[].reviews[].entries[].metadata[].positions`.`ratings`, 'Array(Nullable(String))'))), dynamicElement(`body_json.interests[].entities[].reviews[].entries[].metadata`.`positions`, 'Array(JSON(max_dynamic_types=0, max_dynamic_paths=0))')), dynamicElement(`body_json.interests[].entities[].reviews[].entries`.`metadata`, 'Array(JSON(max_dynamic_types=1, max_dynamic_paths=0))')), dynamicElement(`body_json.interests[].entities[].reviews`.`entries`, 'Array(JSON(max_dynamic_types=2, max_dynamic_paths=0))')), dynamicElement(`body_json.interests[].entities`.`reviews`, 'Array(JSON(max_dynamic_types=4, max_dynamic_paths=0))')), dynamicElement(`body_json.interests`.`entities`, 'Array(JSON(max_dynamic_types=8, max_dynamic_paths=0))')), dynamicElement(body_json.`interests`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, body_json, body_json_promoted, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((arrayExists(`body_json.interests`-> arrayExists(`body_json.interests[].entities`-> arrayExists(`body_json.interests[].entities[].reviews`-> arrayExists(`body_json.interests[].entities[].reviews[].entries`-> arrayExists(`body_json.interests[].entities[].reviews[].entries[].metadata`-> arrayExists(`body_json.interests[].entities[].reviews[].entries[].metadata[].positions`-> (arrayExists(toString(x) -> LOWER(toString(x)) LIKE LOWER(?), dynamicElement(`body_json.interests[].entities[].reviews[].entries[].metadata[].positions`.`ratings`, 'Array(Nullable(Int64))')) OR arrayExists(toString(x) -> toString(x) = ?, dynamicElement(`body_json.interests[].entities[].reviews[].entries[].metadata[].positions`.`ratings`, 'Array(Nullable(Int64))'))), dynamicElement(`body_json.interests[].entities[].reviews[].entries[].metadata`.`positions`, 'Array(JSON(max_dynamic_types=0, max_dynamic_paths=0))')), dynamicElement(`body_json.interests[].entities[].reviews[].entries`.`metadata`, 'Array(JSON(max_dynamic_types=1, max_dynamic_paths=0))')), dynamicElement(`body_json.interests[].entities[].reviews`.`entries`, 'Array(JSON(max_dynamic_types=2, max_dynamic_paths=0))')), dynamicElement(`body_json.interests[].entities`.`reviews`, 'Array(JSON(max_dynamic_types=4, max_dynamic_paths=0))')), dynamicElement(`body_json.interests`.`entities`, 'Array(JSON(max_dynamic_types=8, max_dynamic_paths=0))')), dynamicElement(body_json.`interests`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) OR (arrayExists(`body_json.interests`-> arrayExists(`body_json.interests[].entities`-> arrayExists(`body_json.interests[].entities[].reviews`-> arrayExists(`body_json.interests[].entities[].reviews[].entries`-> arrayExists(`body_json.interests[].entities[].reviews[].entries[].metadata`-> arrayExists(`body_json.interests[].entities[].reviews[].entries[].metadata[].positions`-> (arrayExists(x -> LOWER(x) LIKE LOWER(?), dynamicElement(`body_json.interests[].entities[].reviews[].entries[].metadata[].positions`.`ratings`, 'Array(Nullable(String))')) OR arrayExists(x -> x = ?, dynamicElement(`body_json.interests[].entities[].reviews[].entries[].metadata[].positions`.`ratings`, 'Array(Nullable(String))'))), dynamicElement(`body_json.interests[].entities[].reviews[].entries[].metadata`.`positions`, 'Array(JSON(max_dynamic_types=0, max_dynamic_paths=0))')), dynamicElement(`body_json.interests[].entities[].reviews[].entries`.`metadata`, 'Array(JSON(max_dynamic_types=1, max_dynamic_paths=0))')), dynamicElement(`body_json.interests[].entities[].reviews`.`entries`, 'Array(JSON(max_dynamic_types=2, max_dynamic_paths=0))')), dynamicElement(`body_json.interests[].entities`.`reviews`, 'Array(JSON(max_dynamic_types=4, max_dynamic_paths=0))')), dynamicElement(`body_json.interests`.`entities`, 'Array(JSON(max_dynamic_types=8, max_dynamic_paths=0))')), dynamicElement(body_json.`interests`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Args: []any{uint64(1747945619), uint64(1747983448), "%Good%", "Good", "%Good%", "Good", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
|
||||
Warnings: []string{"Key `interests[].entities[].reviews[].entries[].metadata[].positions[].ratings` is ambiguous, found 2 different combinations of field context / data type: [name=interests[].entities[].reviews[].entries[].metadata[].positions[].ratings,context=body,datatype=[]int64,jsondatatype=Array(Nullable(Int64)) name=interests[].entities[].reviews[].entries[].metadata[].positions[].ratings,context=body,datatype=[]string,jsondatatype=Array(Nullable(String))]."},
|
||||
},
|
||||
@@ -535,7 +492,7 @@ func TestStatementBuilderListQueryBodyPromoted(t *testing.T) {
|
||||
disableBodyJSONQuery(t)
|
||||
}()
|
||||
|
||||
statementBuilder := buildJSONTestStatementBuilder(t, "education", "tags")
|
||||
statementBuilder := buildJSONTestStatementBuilder(t, "education")
|
||||
cases := []struct {
|
||||
name string
|
||||
requestType qbtypes.RequestType
|
||||
@@ -543,20 +500,6 @@ func TestStatementBuilderListQueryBodyPromoted(t *testing.T) {
|
||||
expected qbtypes.Statement
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
name: "Has Array promoted uses body fallback",
|
||||
requestType: qbtypes.RequestTypeRaw,
|
||||
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
|
||||
Signal: telemetrytypes.SignalLogs,
|
||||
Filter: &qbtypes.Filter{Expression: "has(body.tags, 'production')"},
|
||||
Limit: 10,
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, body_json, body_json_promoted, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND has(if(length(dynamicElement(body_json_promoted.`tags`, 'Array(Nullable(String))')) > 0, dynamicElement(body_json_promoted.`tags`, 'Array(Nullable(String))'), dynamicElement(body_json.`tags`, 'Array(Nullable(String))')), ?) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Args: []any{uint64(1747945619), uint64(1747983448), "production", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
|
||||
},
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
name: "Key inside Array(JSON) exists",
|
||||
requestType: qbtypes.RequestTypeRaw,
|
||||
@@ -608,7 +551,7 @@ func TestStatementBuilderListQueryBodyPromoted(t *testing.T) {
|
||||
Limit: 10,
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, body_json, body_json_promoted, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((arrayExists(`body_json.education`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), dynamicElement(`body_json.education`.`parameters`, 'Array(Nullable(Float64))')) OR arrayExists(x -> toFloat64(x) = ?, dynamicElement(`body_json.education`.`parameters`, 'Array(Nullable(Float64))'))), dynamicElement(body_json.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')) OR arrayExists(`body_json_promoted.education`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), dynamicElement(`body_json_promoted.education`.`parameters`, 'Array(Nullable(Float64))')) OR arrayExists(x -> toFloat64(x) = ?, dynamicElement(`body_json_promoted.education`.`parameters`, 'Array(Nullable(Float64))'))), dynamicElement(body_json_promoted.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=256))'))) OR (arrayExists(`body_json.education`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), arrayMap(x->dynamicElement(x, 'Float64'), arrayFilter(x->(dynamicType(x) = 'Float64'), dynamicElement(`body_json.education`.`parameters`, 'Array(Dynamic)')))) OR arrayExists(x -> toFloat64(x) = ?, arrayMap(x->dynamicElement(x, 'Float64'), arrayFilter(x->(dynamicType(x) = 'Float64'), dynamicElement(`body_json.education`.`parameters`, 'Array(Dynamic)'))))), dynamicElement(body_json.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')) OR arrayExists(`body_json_promoted.education`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), arrayMap(x->dynamicElement(x, 'Float64'), arrayFilter(x->(dynamicType(x) = 'Float64'), dynamicElement(`body_json_promoted.education`.`parameters`, 'Array(Dynamic)')))) OR arrayExists(x -> toFloat64(x) = ?, arrayMap(x->dynamicElement(x, 'Float64'), arrayFilter(x->(dynamicType(x) = 'Float64'), dynamicElement(`body_json_promoted.education`.`parameters`, 'Array(Dynamic)'))))), dynamicElement(body_json_promoted.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=256))')))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, body_json, body_json_promoted, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((arrayExists(`body_json.education`-> (arrayExists(toString(x) -> LOWER(toString(x)) LIKE LOWER(?), dynamicElement(`body_json.education`.`parameters`, 'Array(Nullable(Float64))')) OR arrayExists(toFloat64(x) -> toFloat64(x) = ?, dynamicElement(`body_json.education`.`parameters`, 'Array(Nullable(Float64))'))), dynamicElement(body_json.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')) OR arrayExists(`body_json_promoted.education`-> (arrayExists(toString(x) -> LOWER(toString(x)) LIKE LOWER(?), dynamicElement(`body_json_promoted.education`.`parameters`, 'Array(Nullable(Float64))')) OR arrayExists(toFloat64(x) -> toFloat64(x) = ?, dynamicElement(`body_json_promoted.education`.`parameters`, 'Array(Nullable(Float64))'))), dynamicElement(body_json_promoted.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=256))'))) OR (arrayExists(`body_json.education`-> (arrayExists(toString(x) -> LOWER(toString(x)) LIKE LOWER(?), arrayMap(x->dynamicElement(x, 'Float64'), arrayFilter(x->(dynamicType(x) = 'Float64'), dynamicElement(`body_json.education`.`parameters`, 'Array(Dynamic)')))) OR arrayExists(toFloat64(x) -> toFloat64(x) = ?, arrayMap(x->dynamicElement(x, 'Float64'), arrayFilter(x->(dynamicType(x) = 'Float64'), dynamicElement(`body_json.education`.`parameters`, 'Array(Dynamic)'))))), dynamicElement(body_json.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')) OR arrayExists(`body_json_promoted.education`-> (arrayExists(toString(x) -> LOWER(toString(x)) LIKE LOWER(?), arrayMap(x->dynamicElement(x, 'Float64'), arrayFilter(x->(dynamicType(x) = 'Float64'), dynamicElement(`body_json_promoted.education`.`parameters`, 'Array(Dynamic)')))) OR arrayExists(toFloat64(x) -> toFloat64(x) = ?, arrayMap(x->dynamicElement(x, 'Float64'), arrayFilter(x->(dynamicType(x) = 'Float64'), dynamicElement(`body_json_promoted.education`.`parameters`, 'Array(Dynamic)'))))), dynamicElement(body_json_promoted.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=256))')))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Args: []any{uint64(1747945619), uint64(1747983448), "%1.65%", 1.65, "%1.65%", 1.65, "%1.65%", 1.65, "%1.65%", 1.65, "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
|
||||
Warnings: []string{"Key `education[].parameters` is ambiguous, found 2 different combinations of field context / data type: [name=education[].parameters,context=body,datatype=[]float64,materialized=true,jsondatatype=Array(Nullable(Float64)) name=education[].parameters,context=body,datatype=[]dynamic,materialized=true,jsondatatype=Array(Dynamic)]."},
|
||||
},
|
||||
@@ -637,7 +580,7 @@ func TestStatementBuilderListQueryBodyPromoted(t *testing.T) {
|
||||
Limit: 10,
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, body_json, body_json_promoted, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((arrayExists(`body_json.education`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), dynamicElement(`body_json.education`.`parameters`, 'Array(Nullable(Float64))')) OR arrayExists(x -> x = ?, dynamicElement(`body_json.education`.`parameters`, 'Array(Nullable(Float64))'))), dynamicElement(body_json.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')) OR arrayExists(`body_json_promoted.education`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), dynamicElement(`body_json_promoted.education`.`parameters`, 'Array(Nullable(Float64))')) OR arrayExists(x -> x = ?, dynamicElement(`body_json_promoted.education`.`parameters`, 'Array(Nullable(Float64))'))), dynamicElement(body_json_promoted.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=256))'))) OR (arrayExists(`body_json.education`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), arrayMap(x->dynamicElement(x, 'Bool'), arrayFilter(x->(dynamicType(x) = 'Bool'), dynamicElement(`body_json.education`.`parameters`, 'Array(Dynamic)')))) OR arrayExists(x -> x = ?, arrayMap(x->dynamicElement(x, 'Bool'), arrayFilter(x->(dynamicType(x) = 'Bool'), dynamicElement(`body_json.education`.`parameters`, 'Array(Dynamic)'))))), dynamicElement(body_json.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')) OR arrayExists(`body_json_promoted.education`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), arrayMap(x->dynamicElement(x, 'Bool'), arrayFilter(x->(dynamicType(x) = 'Bool'), dynamicElement(`body_json_promoted.education`.`parameters`, 'Array(Dynamic)')))) OR arrayExists(x -> x = ?, arrayMap(x->dynamicElement(x, 'Bool'), arrayFilter(x->(dynamicType(x) = 'Bool'), dynamicElement(`body_json_promoted.education`.`parameters`, 'Array(Dynamic)'))))), dynamicElement(body_json_promoted.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=256))')))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, body_json, body_json_promoted, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((arrayExists(`body_json.education`-> (arrayExists(toString(x) -> LOWER(toString(x)) LIKE LOWER(?), dynamicElement(`body_json.education`.`parameters`, 'Array(Nullable(Float64))')) OR arrayExists(x -> x = ?, dynamicElement(`body_json.education`.`parameters`, 'Array(Nullable(Float64))'))), dynamicElement(body_json.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')) OR arrayExists(`body_json_promoted.education`-> (arrayExists(toString(x) -> LOWER(toString(x)) LIKE LOWER(?), dynamicElement(`body_json_promoted.education`.`parameters`, 'Array(Nullable(Float64))')) OR arrayExists(x -> x = ?, dynamicElement(`body_json_promoted.education`.`parameters`, 'Array(Nullable(Float64))'))), dynamicElement(body_json_promoted.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=256))'))) OR (arrayExists(`body_json.education`-> (arrayExists(toString(x) -> LOWER(toString(x)) LIKE LOWER(?), arrayMap(x->dynamicElement(x, 'Bool'), arrayFilter(x->(dynamicType(x) = 'Bool'), dynamicElement(`body_json.education`.`parameters`, 'Array(Dynamic)')))) OR arrayExists(x -> x = ?, arrayMap(x->dynamicElement(x, 'Bool'), arrayFilter(x->(dynamicType(x) = 'Bool'), dynamicElement(`body_json.education`.`parameters`, 'Array(Dynamic)'))))), dynamicElement(body_json.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')) OR arrayExists(`body_json_promoted.education`-> (arrayExists(toString(x) -> LOWER(toString(x)) LIKE LOWER(?), arrayMap(x->dynamicElement(x, 'Bool'), arrayFilter(x->(dynamicType(x) = 'Bool'), dynamicElement(`body_json_promoted.education`.`parameters`, 'Array(Dynamic)')))) OR arrayExists(x -> x = ?, arrayMap(x->dynamicElement(x, 'Bool'), arrayFilter(x->(dynamicType(x) = 'Bool'), dynamicElement(`body_json_promoted.education`.`parameters`, 'Array(Dynamic)'))))), dynamicElement(body_json_promoted.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=256))')))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Args: []any{uint64(1747945619), uint64(1747983448), "%true%", true, "%true%", true, "%true%", true, "%true%", true, "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
|
||||
Warnings: []string{"Key `education[].parameters` is ambiguous, found 2 different combinations of field context / data type: [name=education[].parameters,context=body,datatype=[]float64,materialized=true,jsondatatype=Array(Nullable(Float64)) name=education[].parameters,context=body,datatype=[]dynamic,materialized=true,jsondatatype=Array(Dynamic)]."},
|
||||
},
|
||||
@@ -652,7 +595,7 @@ func TestStatementBuilderListQueryBodyPromoted(t *testing.T) {
|
||||
Limit: 10,
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, body_json, body_json_promoted, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((arrayExists(`body_json.education`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), dynamicElement(`body_json.education`.`parameters`, 'Array(Nullable(Float64))')) OR arrayExists(x -> toString(x) = ?, dynamicElement(`body_json.education`.`parameters`, 'Array(Nullable(Float64))'))), dynamicElement(body_json.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')) OR arrayExists(`body_json_promoted.education`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), dynamicElement(`body_json_promoted.education`.`parameters`, 'Array(Nullable(Float64))')) OR arrayExists(x -> toString(x) = ?, dynamicElement(`body_json_promoted.education`.`parameters`, 'Array(Nullable(Float64))'))), dynamicElement(body_json_promoted.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=256))'))) OR (arrayExists(`body_json.education`-> (arrayExists(x -> LOWER(x) LIKE LOWER(?), arrayMap(x->dynamicElement(x, 'String'), arrayFilter(x->(dynamicType(x) = 'String'), dynamicElement(`body_json.education`.`parameters`, 'Array(Dynamic)')))) OR arrayExists(x -> x = ?, arrayMap(x->dynamicElement(x, 'String'), arrayFilter(x->(dynamicType(x) = 'String'), dynamicElement(`body_json.education`.`parameters`, 'Array(Dynamic)'))))), dynamicElement(body_json.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')) OR arrayExists(`body_json_promoted.education`-> (arrayExists(x -> LOWER(x) LIKE LOWER(?), arrayMap(x->dynamicElement(x, 'String'), arrayFilter(x->(dynamicType(x) = 'String'), dynamicElement(`body_json_promoted.education`.`parameters`, 'Array(Dynamic)')))) OR arrayExists(x -> x = ?, arrayMap(x->dynamicElement(x, 'String'), arrayFilter(x->(dynamicType(x) = 'String'), dynamicElement(`body_json_promoted.education`.`parameters`, 'Array(Dynamic)'))))), dynamicElement(body_json_promoted.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=256))')))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, body_json, body_json_promoted, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((arrayExists(`body_json.education`-> (arrayExists(toString(x) -> LOWER(toString(x)) LIKE LOWER(?), dynamicElement(`body_json.education`.`parameters`, 'Array(Nullable(Float64))')) OR arrayExists(toString(x) -> toString(x) = ?, dynamicElement(`body_json.education`.`parameters`, 'Array(Nullable(Float64))'))), dynamicElement(body_json.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')) OR arrayExists(`body_json_promoted.education`-> (arrayExists(toString(x) -> LOWER(toString(x)) LIKE LOWER(?), dynamicElement(`body_json_promoted.education`.`parameters`, 'Array(Nullable(Float64))')) OR arrayExists(toString(x) -> toString(x) = ?, dynamicElement(`body_json_promoted.education`.`parameters`, 'Array(Nullable(Float64))'))), dynamicElement(body_json_promoted.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=256))'))) OR (arrayExists(`body_json.education`-> (arrayExists(x -> LOWER(x) LIKE LOWER(?), arrayMap(x->dynamicElement(x, 'String'), arrayFilter(x->(dynamicType(x) = 'String'), dynamicElement(`body_json.education`.`parameters`, 'Array(Dynamic)')))) OR arrayExists(x -> x = ?, arrayMap(x->dynamicElement(x, 'String'), arrayFilter(x->(dynamicType(x) = 'String'), dynamicElement(`body_json.education`.`parameters`, 'Array(Dynamic)'))))), dynamicElement(body_json.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')) OR arrayExists(`body_json_promoted.education`-> (arrayExists(x -> LOWER(x) LIKE LOWER(?), arrayMap(x->dynamicElement(x, 'String'), arrayFilter(x->(dynamicType(x) = 'String'), dynamicElement(`body_json_promoted.education`.`parameters`, 'Array(Dynamic)')))) OR arrayExists(x -> x = ?, arrayMap(x->dynamicElement(x, 'String'), arrayFilter(x->(dynamicType(x) = 'String'), dynamicElement(`body_json_promoted.education`.`parameters`, 'Array(Dynamic)'))))), dynamicElement(body_json_promoted.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=256))')))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Args: []any{uint64(1747945619), uint64(1747983448), "%passed%", "passed", "%passed%", "passed", "%passed%", "passed", "%passed%", "passed", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
|
||||
Warnings: []string{"Key `education[].parameters` is ambiguous, found 2 different combinations of field context / data type: [name=education[].parameters,context=body,datatype=[]float64,materialized=true,jsondatatype=Array(Nullable(Float64)) name=education[].parameters,context=body,datatype=[]dynamic,materialized=true,jsondatatype=Array(Dynamic)]."},
|
||||
},
|
||||
|
||||
@@ -3,12 +3,12 @@ package telemetrylogs
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
func parseStrValue(valueStr string, operator qbtypes.FilterOperator) (telemetrytypes.FieldDataType, any) {
|
||||
@@ -41,55 +41,31 @@ func parseStrValue(valueStr string, operator qbtypes.FilterOperator) (telemetryt
|
||||
}
|
||||
|
||||
func InferDataType(value any, operator qbtypes.FilterOperator, key *telemetrytypes.TelemetryFieldKey) (telemetrytypes.FieldDataType, any) {
|
||||
if operator.IsArrayOperator() && reflect.ValueOf(value).Kind() != reflect.Slice {
|
||||
value = []any{value}
|
||||
}
|
||||
|
||||
// closure to calculate the data type of the value
|
||||
var closure func(value any, key *telemetrytypes.TelemetryFieldKey) (telemetrytypes.FieldDataType, any)
|
||||
closure = func(value any, key *telemetrytypes.TelemetryFieldKey) (telemetrytypes.FieldDataType, any) {
|
||||
// check if the value is a int, float, string, bool
|
||||
valueType := telemetrytypes.FieldDataTypeUnspecified
|
||||
switch v := value.(type) {
|
||||
case []any:
|
||||
// take the first element and infer the type
|
||||
var scalerType telemetrytypes.FieldDataType
|
||||
if len(v) > 0 {
|
||||
// Note: [[...]] Slices inside Slices are not handled yet
|
||||
if reflect.ValueOf(v[0]).Kind() == reflect.Slice {
|
||||
return telemetrytypes.FieldDataTypeUnspecified, value
|
||||
}
|
||||
|
||||
scalerType, _ = closure(v[0], key)
|
||||
}
|
||||
|
||||
arrayType := telemetrytypes.ScalerFieldTypeToArrayFieldType[scalerType]
|
||||
switch {
|
||||
// decide on the field data type based on the key
|
||||
case key.FieldDataType.IsArray():
|
||||
return arrayType, v
|
||||
default:
|
||||
// TODO(Piyush): backward compatibility for the old String based JSON QB queries
|
||||
if strings.HasSuffix(key.Name, telemetrytypes.ArrayAnyIndexSuffix) {
|
||||
return arrayType, v
|
||||
}
|
||||
return scalerType, v
|
||||
}
|
||||
case uint8, uint16, uint32, uint64, int, int8, int16, int32, int64:
|
||||
valueType = telemetrytypes.FieldDataTypeInt64
|
||||
case float32, float64:
|
||||
valueType = telemetrytypes.FieldDataTypeFloat64
|
||||
case string:
|
||||
valueType, value = parseStrValue(v, operator)
|
||||
case bool:
|
||||
valueType = telemetrytypes.FieldDataTypeBool
|
||||
// check if the value is a int, float, string, bool
|
||||
valueType := telemetrytypes.FieldDataTypeUnspecified
|
||||
switch v := value.(type) {
|
||||
case []any:
|
||||
// take the first element and infer the type
|
||||
if len(v) > 0 {
|
||||
valueType, _ = InferDataType(v[0], operator, key)
|
||||
}
|
||||
|
||||
return valueType, value
|
||||
return valueType, v
|
||||
case uint8, uint16, uint32, uint64, int, int8, int16, int32, int64:
|
||||
valueType = telemetrytypes.FieldDataTypeInt64
|
||||
case float32, float64:
|
||||
valueType = telemetrytypes.FieldDataTypeFloat64
|
||||
case string:
|
||||
valueType, value = parseStrValue(v, operator)
|
||||
case bool:
|
||||
valueType = telemetrytypes.FieldDataTypeBool
|
||||
}
|
||||
|
||||
// calculate the data type of the value
|
||||
return closure(value, key)
|
||||
// check if it is array
|
||||
if strings.HasSuffix(key.Name, "[*]") || strings.HasSuffix(key.Name, "[]") {
|
||||
valueType = telemetrytypes.FieldDataType{String: valuer.NewString(fmt.Sprintf("[]%s", valueType.StringValue()))}
|
||||
}
|
||||
|
||||
return valueType, value
|
||||
}
|
||||
|
||||
func getBodyJSONPath(key *telemetrytypes.TelemetryFieldKey) string {
|
||||
|
||||
@@ -421,38 +421,6 @@ func TestStatementBuilderListQueryResourceTests(t *testing.T) {
|
||||
},
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
name: "IN operator with json search",
|
||||
requestType: qbtypes.RequestTypeRaw,
|
||||
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
|
||||
Signal: telemetrytypes.SignalLogs,
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "body.user_names[*] IN 'john_doe'",
|
||||
},
|
||||
Limit: 10,
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((JSONExtract(JSON_QUERY(body, '$.\"user_names\"[*]'), 'Array(String)') = ?) AND JSON_EXISTS(body, '$.\"user_names\"[*]')) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Args: []any{uint64(1747945619), uint64(1747983448), "john_doe", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
|
||||
},
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
name: "has with json search",
|
||||
requestType: qbtypes.RequestTypeRaw,
|
||||
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
|
||||
Signal: telemetrytypes.SignalLogs,
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "has(body.user_names[*], 'john_doe')",
|
||||
},
|
||||
Limit: 10,
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND has(JSONExtract(JSON_QUERY(body, '$.\"user_names\"[*]'), 'Array(String)'), ?) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Args: []any{uint64(1747945619), uint64(1747983448), "john_doe", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
|
||||
},
|
||||
expectedErr: nil,
|
||||
},
|
||||
}
|
||||
|
||||
mockMetadataStore := telemetrytypestest.NewMockMetadataStore()
|
||||
|
||||
@@ -112,8 +112,7 @@ func (t *telemetryMetaStore) buildBodyJSONPaths(ctx context.Context,
|
||||
}
|
||||
|
||||
for _, fieldKey := range fieldKeys {
|
||||
promotedKey := strings.Split(fieldKey.Name, telemetrytypes.ArraySep)[0]
|
||||
fieldKey.Materialized = promoted.Contains(promotedKey)
|
||||
fieldKey.Materialized = promoted.Contains(fieldKey.Name)
|
||||
fieldKey.Indexes = indexes[fieldKey.Name]
|
||||
}
|
||||
|
||||
@@ -502,8 +501,7 @@ func (t *telemetryMetaStore) GetPromotedPaths(ctx context.Context, paths ...stri
|
||||
sb := sqlbuilder.Select("path").From(fmt.Sprintf("%s.%s", DBName, PromotedPathsTableName))
|
||||
pathConditions := []string{}
|
||||
for _, path := range paths {
|
||||
split := strings.Split(path, telemetrytypes.ArraySep)
|
||||
pathConditions = append(pathConditions, sb.Equal("path", split[0]))
|
||||
pathConditions = append(pathConditions, sb.Equal("path", path))
|
||||
}
|
||||
sb.Where(sb.Or(pathConditions...))
|
||||
|
||||
|
||||
79
pkg/transition/filter_migrate.go
Normal file
79
pkg/transition/filter_migrate.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package transition
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
|
||||
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
|
||||
)
|
||||
|
||||
// BuildFilterExpressionFromFilterSet converts a v3-style FilterSet JSON
|
||||
// ({"op": "...", "items": [...]}) into a v5-style filter expression string
|
||||
// (for example: "attribute.http.method = 'GET' AND resource.env = 'prod'").
|
||||
//
|
||||
// It reuses migrateCommon.createFilterExpression so that all the existing
|
||||
// semantics around operators, variables, data types, and ambiguity handling
|
||||
// are preserved.
|
||||
//
|
||||
// dataSource determines which ambiguity set to use ("logs", "traces", etc.).
|
||||
// For log pipelines, pass "logs".
|
||||
//
|
||||
// Returns:
|
||||
// - expression: the generated filter expression string
|
||||
// - migrated: true if an expression was generated, false if there was
|
||||
// nothing to migrate (e.g. empty filters)
|
||||
// - err: non-nil only if the input JSON could not be parsed
|
||||
func BuildFilterExpressionFromFilterSet(
|
||||
ctx context.Context,
|
||||
logger *slog.Logger,
|
||||
dataSource string,
|
||||
filterSet *v3.FilterSet,
|
||||
) (expression string, migrated bool, err error) {
|
||||
if filterSet == nil {
|
||||
return "", false, nil
|
||||
}
|
||||
filterJSON, err := json.Marshal(filterSet)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
|
||||
var filters map[string]any
|
||||
if err := json.Unmarshal([]byte(filterJSON), &filters); err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
|
||||
mc := NewMigrateCommon(logger)
|
||||
// add keys with type into ambiguity set to preserve context in generated expression
|
||||
for _, item := range filterSet.Items {
|
||||
if item.Key.Type == v3.AttributeKeyTypeUnspecified {
|
||||
continue
|
||||
}
|
||||
|
||||
mc.ambiguity[dataSource] = append(mc.ambiguity[dataSource], item.Key.Key)
|
||||
}
|
||||
|
||||
// Shape expected by migrateCommon.createFilterExpression:
|
||||
// queryData["dataSource"] == "logs" | "traces" | "metrics"
|
||||
// queryData["filters"] == map[string]any{"op": "...", "items": [...]}
|
||||
queryData := map[string]any{
|
||||
"dataSource": dataSource,
|
||||
"filters": filters,
|
||||
}
|
||||
|
||||
if !mc.createFilterExpression(ctx, queryData) {
|
||||
return "", false, nil
|
||||
}
|
||||
|
||||
filterAny, ok := queryData["filter"].(map[string]any)
|
||||
if !ok {
|
||||
return "", false, nil
|
||||
}
|
||||
|
||||
expr, ok := filterAny["expression"].(string)
|
||||
if !ok || expr == "" {
|
||||
return "", false, nil
|
||||
}
|
||||
|
||||
return expr, true, nil
|
||||
}
|
||||
220
pkg/transition/filter_migrate_test.go
Normal file
220
pkg/transition/filter_migrate_test.go
Normal file
@@ -0,0 +1,220 @@
|
||||
package transition
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"testing"
|
||||
|
||||
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
|
||||
)
|
||||
|
||||
func TestBuildFilterExpressionFromFilterSet(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
logger := slog.Default()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
dataSource string
|
||||
filterSet *v3.FilterSet
|
||||
wantExpr string
|
||||
wantMigrated bool
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "empty filter JSON",
|
||||
dataSource: "logs",
|
||||
filterSet: &v3.FilterSet{},
|
||||
wantExpr: "",
|
||||
wantMigrated: false,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "empty items array",
|
||||
dataSource: "logs",
|
||||
filterSet: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{}},
|
||||
wantExpr: "",
|
||||
wantMigrated: false,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "multiple filter items with AND operator",
|
||||
dataSource: "logs",
|
||||
filterSet: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{Key: "http.method", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag},
|
||||
Operator: v3.FilterOperatorEqual,
|
||||
Value: "GET",
|
||||
},
|
||||
{
|
||||
Key: v3.AttributeKey{Key: "environment", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeResource},
|
||||
Operator: v3.FilterOperatorEqual,
|
||||
Value: "prod",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantExpr: `(attribute.http.method = 'GET' AND resource.environment = 'prod')`,
|
||||
wantMigrated: true,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "multiple filter items with OR operator",
|
||||
dataSource: "logs",
|
||||
filterSet: &v3.FilterSet{
|
||||
Operator: "OR",
|
||||
Items: []v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{Key: "level", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag},
|
||||
Operator: v3.FilterOperatorEqual,
|
||||
Value: "error",
|
||||
},
|
||||
{
|
||||
Key: v3.AttributeKey{Key: "level", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag},
|
||||
Operator: v3.FilterOperatorEqual,
|
||||
Value: "warn",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantExpr: `(attribute.level = 'error' OR attribute.level = 'warn')`,
|
||||
wantMigrated: true,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "in operator with array value",
|
||||
dataSource: "logs",
|
||||
filterSet: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{{
|
||||
Key: v3.AttributeKey{Key: "service.name", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag},
|
||||
Operator: v3.FilterOperatorIn,
|
||||
Value: []string{"api", "web", "worker"},
|
||||
}},
|
||||
},
|
||||
wantExpr: `attribute.service.name IN ['api', 'web', 'worker']`,
|
||||
wantMigrated: true,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "contains operator",
|
||||
dataSource: "logs",
|
||||
filterSet: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{{
|
||||
Key: v3.AttributeKey{Key: "message", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag},
|
||||
Operator: v3.FilterOperatorContains,
|
||||
Value: "error",
|
||||
}},
|
||||
},
|
||||
wantExpr: `attribute.message CONTAINS 'error'`,
|
||||
wantMigrated: true,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "not exists operator",
|
||||
dataSource: "logs",
|
||||
filterSet: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{{
|
||||
Key: v3.AttributeKey{Key: "trace.id", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag},
|
||||
Operator: v3.FilterOperatorNotExists,
|
||||
Value: nil,
|
||||
}},
|
||||
},
|
||||
wantExpr: `attribute.trace.id NOT EXISTS`,
|
||||
wantMigrated: true,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "regex operator",
|
||||
dataSource: "logs",
|
||||
filterSet: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{{
|
||||
Key: v3.AttributeKey{Key: "body"},
|
||||
Operator: v3.FilterOperatorRegex,
|
||||
Value: ".*",
|
||||
}},
|
||||
},
|
||||
wantExpr: `body REGEXP '.*'`,
|
||||
wantMigrated: true,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "has operator",
|
||||
dataSource: "logs",
|
||||
filterSet: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{{
|
||||
Key: v3.AttributeKey{Key: "tags", DataType: v3.AttributeKeyDataTypeArrayString, Type: v3.AttributeKeyTypeTag},
|
||||
Operator: v3.FilterOperatorHas,
|
||||
Value: "production",
|
||||
}},
|
||||
},
|
||||
wantExpr: `has(attribute.tags, 'production')`,
|
||||
wantMigrated: true,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "complex filter with multiple operators",
|
||||
dataSource: "logs",
|
||||
filterSet: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{Key: "http.method", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag},
|
||||
Operator: v3.FilterOperatorEqual,
|
||||
Value: "POST",
|
||||
},
|
||||
{
|
||||
Key: v3.AttributeKey{Key: "http.status_code", DataType: v3.AttributeKeyDataTypeInt64, Type: v3.AttributeKeyTypeTag},
|
||||
Operator: v3.FilterOperatorGreaterThanOrEq,
|
||||
Value: float64(400),
|
||||
},
|
||||
{
|
||||
Key: v3.AttributeKey{Key: "resource.env", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeResource},
|
||||
Operator: v3.FilterOperatorEqual,
|
||||
Value: "prod",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantExpr: `(attribute.http.method = 'POST' AND attribute.http.status_code >= 400 AND resource.resource.env = 'prod')`,
|
||||
wantMigrated: true,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "filter with resource type (non-ambiguous key)",
|
||||
dataSource: "logs",
|
||||
filterSet: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{{
|
||||
Key: v3.AttributeKey{Key: "service.name", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeResource},
|
||||
Operator: v3.FilterOperatorEqual,
|
||||
Value: "frontend",
|
||||
}},
|
||||
},
|
||||
wantExpr: `resource.service.name = 'frontend'`,
|
||||
wantMigrated: true,
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
expr, migrated, err := BuildFilterExpressionFromFilterSet(ctx, logger, tt.dataSource, tt.filterSet)
|
||||
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("BuildFilterExpressionFromFilterSet() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
|
||||
if migrated != tt.wantMigrated {
|
||||
t.Errorf("BuildFilterExpressionFromFilterSet() migrated = %v, want %v", migrated, tt.wantMigrated)
|
||||
}
|
||||
|
||||
if expr != tt.wantExpr {
|
||||
t.Errorf("BuildFilterExpressionFromFilterSet() expression = %v, want %v", expr, tt.wantExpr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,8 @@ type migrateCommon struct {
|
||||
|
||||
func NewMigrateCommon(logger *slog.Logger) *migrateCommon {
|
||||
return &migrateCommon{
|
||||
logger: logger,
|
||||
logger: logger,
|
||||
ambiguity: make(map[string][]string),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,9 +7,9 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/queryBuilderToExpr"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
@@ -57,7 +57,7 @@ type StoreablePipeline struct {
|
||||
|
||||
type GettablePipeline struct {
|
||||
StoreablePipeline
|
||||
Filter *v3.FilterSet `json:"filter"`
|
||||
Filter *qbtypes.Filter `json:"filter"`
|
||||
Config []PipelineOperator `json:"config"`
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ func (i *GettablePipeline) ParseRawConfig() error {
|
||||
}
|
||||
|
||||
func (i *GettablePipeline) ParseFilter() error {
|
||||
f := v3.FilterSet{}
|
||||
f := qbtypes.Filter{}
|
||||
err := json.Unmarshal([]byte(i.FilterString), &f)
|
||||
if err != nil {
|
||||
return errors.WrapInvalidInputf(err, errors.CodeInvalidInput, "failed to parse filter")
|
||||
@@ -200,7 +200,7 @@ type PostablePipeline struct {
|
||||
Alias string `json:"alias"`
|
||||
Description string `json:"description"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Filter *v3.FilterSet `json:"filter"`
|
||||
Filter *qbtypes.Filter `json:"filter"`
|
||||
Config []PipelineOperator `json:"config"`
|
||||
}
|
||||
|
||||
@@ -218,6 +218,14 @@ func (p *PostablePipeline) IsValid() error {
|
||||
}
|
||||
|
||||
// check the filter
|
||||
if p.Filter == nil || strings.TrimSpace(p.Filter.Expression) == "" {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "filter.expression is required")
|
||||
}
|
||||
|
||||
// Validate that every field in the expression has an explicit context
|
||||
// (attribute., resource., body., etc) so later pipeline processing does not
|
||||
// need to guess. We do not validate that the field actually
|
||||
// exists – only that the context is specified.
|
||||
_, err := queryBuilderToExpr.Parse(p.Filter)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -3,24 +3,13 @@ package pipelinetypes
|
||||
import (
|
||||
"testing"
|
||||
|
||||
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestIsValidPostablePipeline(t *testing.T) {
|
||||
validPipelineFilterSet := &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "method",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
},
|
||||
Operator: "=",
|
||||
Value: "GET",
|
||||
},
|
||||
},
|
||||
validPipelineFilter := &qbtypes.Filter{
|
||||
Expression: `attribute.method = "GET"`,
|
||||
}
|
||||
|
||||
var correctQueriesTest = []struct {
|
||||
@@ -34,7 +23,7 @@ func TestIsValidPostablePipeline(t *testing.T) {
|
||||
Name: "pipeline 1",
|
||||
Alias: "pipeline1",
|
||||
Enabled: true,
|
||||
Filter: validPipelineFilterSet,
|
||||
Filter: validPipelineFilter,
|
||||
Config: []PipelineOperator{},
|
||||
},
|
||||
IsValid: false,
|
||||
@@ -46,7 +35,7 @@ func TestIsValidPostablePipeline(t *testing.T) {
|
||||
Name: "pipeline 1",
|
||||
Alias: "pipeline1",
|
||||
Enabled: true,
|
||||
Filter: validPipelineFilterSet,
|
||||
Filter: validPipelineFilter,
|
||||
Config: []PipelineOperator{},
|
||||
},
|
||||
IsValid: false,
|
||||
@@ -58,7 +47,7 @@ func TestIsValidPostablePipeline(t *testing.T) {
|
||||
Name: "pipeline 1",
|
||||
Alias: "pipeline1",
|
||||
Enabled: true,
|
||||
Filter: validPipelineFilterSet,
|
||||
Filter: validPipelineFilter,
|
||||
Config: []PipelineOperator{},
|
||||
},
|
||||
IsValid: true,
|
||||
@@ -70,19 +59,21 @@ func TestIsValidPostablePipeline(t *testing.T) {
|
||||
Name: "pipeline 1",
|
||||
Alias: "pipeline1",
|
||||
Enabled: true,
|
||||
Filter: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "method",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeUnspecified,
|
||||
},
|
||||
Operator: "regex",
|
||||
Value: "[0-9A-Z*",
|
||||
},
|
||||
},
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "",
|
||||
},
|
||||
},
|
||||
IsValid: false,
|
||||
},
|
||||
{
|
||||
Name: "Filter without context prefix on field",
|
||||
Pipeline: PostablePipeline{
|
||||
OrderID: 1,
|
||||
Name: "pipeline 1",
|
||||
Alias: "pipeline1",
|
||||
Enabled: true,
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: `method = "GET"`,
|
||||
},
|
||||
},
|
||||
IsValid: false,
|
||||
@@ -94,19 +85,19 @@ func TestIsValidPostablePipeline(t *testing.T) {
|
||||
Name: "pipeline 1",
|
||||
Alias: "pipeline1",
|
||||
Enabled: true,
|
||||
Filter: validPipelineFilterSet,
|
||||
Filter: validPipelineFilter,
|
||||
},
|
||||
IsValid: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range correctQueriesTest {
|
||||
Convey(test.Name, t, func() {
|
||||
t.Run(test.Name, func(t *testing.T) {
|
||||
err := test.Pipeline.IsValid()
|
||||
if test.IsValid {
|
||||
So(err, ShouldBeNil)
|
||||
assert.NoError(t, err)
|
||||
} else {
|
||||
So(err, ShouldBeError)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -365,12 +356,12 @@ var operatorTest = []struct {
|
||||
|
||||
func TestValidOperator(t *testing.T) {
|
||||
for _, test := range operatorTest {
|
||||
Convey(test.Name, t, func() {
|
||||
t.Run(test.Name, func(t *testing.T) {
|
||||
err := isValidOperator(test.Operator)
|
||||
if test.IsValid {
|
||||
So(err, ShouldBeNil)
|
||||
assert.NoError(t, err)
|
||||
} else {
|
||||
So(err, ShouldBeError)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -161,17 +161,6 @@ func (f FilterOperator) IsStringSearchOperator() bool {
|
||||
}
|
||||
}
|
||||
|
||||
// IsArrayOperator returns true if the operator works with array values only
|
||||
func (f FilterOperator) IsArrayOperator() bool {
|
||||
switch f {
|
||||
case FilterOperatorIn, FilterOperatorNotIn,
|
||||
FilterOperatorBetween, FilterOperatorNotBetween:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
type OrderDirection struct {
|
||||
valuer.String
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
package querybuildertypesv5
|
||||
|
||||
import "github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
|
||||
// LogicalOp represents how child expressions are combined.
|
||||
type LogicalOp string
|
||||
|
||||
const (
|
||||
// LogicalOpLeaf represents a leaf node containing one or more simple conditions.
|
||||
LogicalOpLeaf LogicalOp = "LEAF"
|
||||
// LogicalOpAnd represents an AND combination of children.
|
||||
LogicalOpAnd LogicalOp = "AND"
|
||||
// LogicalOpOr represents an OR combination of children.
|
||||
LogicalOpOr LogicalOp = "OR"
|
||||
)
|
||||
|
||||
// FilterExprNode is a reusable logical representation of a filter expression.
|
||||
//
|
||||
// - Leaf nodes (Op == LogicalOpLeaf) contain one or more ParsedFilterCondition.
|
||||
// - Non-leaf nodes (Op == LogicalOpAnd/LogicalOpOr) contain Children.
|
||||
// - Negated indicates a leading NOT applied to this subtree.
|
||||
type FilterExprNode struct {
|
||||
Op LogicalOp
|
||||
Negated bool
|
||||
Conditions []FilterCondition
|
||||
Children []*FilterExprNode
|
||||
}
|
||||
|
||||
func NewEmptyFilterExprNode() *FilterExprNode {
|
||||
return &FilterExprNode{
|
||||
Op: LogicalOpLeaf,
|
||||
}
|
||||
}
|
||||
|
||||
func (f *FilterExprNode) Flatten() []FilterCondition {
|
||||
var conditions []FilterCondition
|
||||
var walk func(node *FilterExprNode)
|
||||
|
||||
walk = func(node *FilterExprNode) {
|
||||
if node == nil {
|
||||
return
|
||||
}
|
||||
if node.Op == LogicalOpLeaf {
|
||||
conditions = append(conditions, node.Conditions...)
|
||||
}
|
||||
for _, child := range node.Children {
|
||||
walk(child)
|
||||
}
|
||||
}
|
||||
|
||||
walk(f)
|
||||
return conditions
|
||||
}
|
||||
|
||||
// FilterCondition represents a single comparison or existence check
|
||||
// extracted from a filter expression.
|
||||
//
|
||||
// - Keys: one or more logical field keys on the left-hand side (see where_clause_visitor.VisitKey
|
||||
// for why one expression key can resolve to multiple TelemetryFieldKeys).
|
||||
// - Op: filter operator (e.g. =, !=, in, exists, between).
|
||||
// - Value: right-hand side literal (any type: single value, slice for IN/NOT IN, nil for EXISTS, etc.).
|
||||
type FilterCondition struct {
|
||||
Key *telemetrytypes.TelemetryFieldKey
|
||||
Op FilterOperator
|
||||
Value any
|
||||
}
|
||||
@@ -23,10 +23,8 @@ const (
|
||||
// e.g., "body.status" where "body." is the prefix
|
||||
BodyJSONStringSearchPrefix = "body."
|
||||
ArraySep = jsontypeexporter.ArraySeparator
|
||||
ArraySepSuffix = "[]"
|
||||
// TODO(Piyush): Remove once we've migrated to the new array syntax
|
||||
ArrayAnyIndex = "[*]."
|
||||
ArrayAnyIndexSuffix = "[*]"
|
||||
ArrayAnyIndex = "[*]."
|
||||
)
|
||||
|
||||
type TelemetryFieldKey struct {
|
||||
|
||||
@@ -93,14 +93,6 @@ var (
|
||||
FieldDataTypeArrayFloat64: "Array(Float64)",
|
||||
FieldDataTypeArrayBool: "Array(Bool)",
|
||||
}
|
||||
|
||||
ScalerFieldTypeToArrayFieldType = map[FieldDataType]FieldDataType{
|
||||
FieldDataTypeString: FieldDataTypeArrayString,
|
||||
FieldDataTypeBool: FieldDataTypeArrayBool,
|
||||
FieldDataTypeNumber: FieldDataTypeArrayNumber,
|
||||
FieldDataTypeInt64: FieldDataTypeArrayInt64,
|
||||
FieldDataTypeFloat64: FieldDataTypeArrayFloat64,
|
||||
}
|
||||
)
|
||||
|
||||
func (f FieldDataType) CHDataType() string {
|
||||
|
||||
@@ -65,7 +65,6 @@ func TestJSONTypeSet() (map[string][]JSONDataType, MetadataStore) {
|
||||
"interests[].entities[].reviews[].entries[].metadata[].positions[].unit": {String},
|
||||
"interests[].entities[].reviews[].entries[].metadata[].positions[].ratings": {ArrayInt64, ArrayString},
|
||||
"message": {String},
|
||||
"tags": {ArrayString},
|
||||
}
|
||||
|
||||
return types, nil
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
from http import HTTPStatus
|
||||
from typing import Callable
|
||||
|
||||
import requests
|
||||
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
|
||||
from fixtures.types import SigNoz
|
||||
|
||||
DUPLICATE_USER_EMAIL = "duplicate@integration.test"
|
||||
|
||||
|
||||
def test_duplicate_user_invite_rejected(
|
||||
signoz: SigNoz,
|
||||
get_token: Callable[[str, str], str],
|
||||
):
|
||||
"""
|
||||
Verify that the unique index on (email, org_id) in the users table prevents
|
||||
creating duplicate users. This invites a new user, accepts the invite, then
|
||||
tries to invite and accept the same email again expecting a failure.
|
||||
"""
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
# Step 1: Invite a new user.
|
||||
initial_invite_response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/invite"),
|
||||
json={"email": DUPLICATE_USER_EMAIL, "role": "EDITOR"},
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=2,
|
||||
)
|
||||
assert initial_invite_response.status_code == HTTPStatus.CREATED
|
||||
initial_invite_token = initial_invite_response.json()["data"]["token"]
|
||||
|
||||
# Step 2: Accept the invite to create the user.
|
||||
initial_accept_response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/invite/accept"),
|
||||
json={"token": initial_invite_token, "password": "password123Z$"},
|
||||
timeout=2,
|
||||
)
|
||||
assert initial_accept_response.status_code == HTTPStatus.CREATED
|
||||
|
||||
# Step 3: Invite the same email again.
|
||||
duplicate_invite_response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/invite"),
|
||||
json={"email": DUPLICATE_USER_EMAIL, "role": "VIEWER"},
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=2,
|
||||
)
|
||||
|
||||
# The invite creation itself may be rejected if the app checks for existing users.
|
||||
if duplicate_invite_response.status_code != HTTPStatus.CREATED:
|
||||
assert duplicate_invite_response.status_code == HTTPStatus.CONFLICT
|
||||
return
|
||||
|
||||
duplicate_invite_token = duplicate_invite_response.json()["data"]["token"]
|
||||
|
||||
# Step 4: Accept the duplicate invite — should fail due to unique constraint.
|
||||
duplicate_accept_response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/invite/accept"),
|
||||
json={"token": duplicate_invite_token, "password": "password123Z$"},
|
||||
timeout=2,
|
||||
)
|
||||
assert duplicate_accept_response.status_code == HTTPStatus.CONFLICT
|
||||
Reference in New Issue
Block a user