Compare commits

..

1 Commits

Author SHA1 Message Date
Ankit Nayan
27a43dc6d2 enable recommendedsecondary_indices_enable_bulk_filtering 2026-05-21 14:49:00 +05:30
74 changed files with 101 additions and 1701 deletions

View File

@@ -1,147 +0,0 @@
import { renderHook } from '@testing-library/react';
import { UseQueryResult } from 'react-query';
import { SuccessResponse } from 'types/api';
import { Widgets } from 'types/api/dashboard/getAll';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { usePanelContextMenu } from '../usePanelContextMenu';
// The hook composes `useCoordinates` (popover state) and `useGraphContextMenu`
// (menu items). We mock both so the test focuses on the `enableDrillDown` gate
// rather than the implementation of the menu wiring itself.
const onClickMock = jest.fn();
jest.mock('periscope/components/ContextMenu', () => ({
useCoordinates: (): unknown => ({
coordinates: null,
popoverPosition: null,
clickedData: null,
onClose: jest.fn(),
subMenu: null,
onClick: onClickMock,
setSubMenu: jest.fn(),
}),
}));
jest.mock('container/QueryTable/Drilldown/useGraphContextMenu', () => ({
__esModule: true,
default: (): { menuItemsConfig: { header: string; items: string } } => ({
menuItemsConfig: { header: 'menu-header', items: 'menu-items' },
}),
}));
jest.mock('container/QueryTable/Drilldown/drilldownUtils', () => ({
getUplotClickData: jest.fn(() => ({
coord: { x: 1, y: 2 },
record: { queryName: 'A', filters: [] },
label: 'lbl',
seriesColor: '#abc',
})),
}));
jest.mock('container/PanelWrapper/utils', () => ({
isApmMetric: jest.fn(() => false),
getTimeRangeFromStepInterval: jest.fn(() => ({ start: 0, end: 0 })),
}));
const mockWidget = { id: 'w-1', query: {} } as unknown as Widgets;
const mockQueryResponse = {
data: undefined,
isLoading: false,
} as unknown as UseQueryResult<
SuccessResponse<MetricRangePayloadProps, unknown>,
Error
>;
describe('usePanelContextMenu', () => {
beforeEach(() => {
onClickMock.mockClear();
});
it('returns empty menuItemsConfig when enableDrillDown is false', () => {
const { result } = renderHook(() =>
usePanelContextMenu({
widget: mockWidget,
queryResponse: mockQueryResponse,
enableDrillDown: false,
}),
);
expect(result.current.menuItemsConfig).toStrictEqual({});
});
it('returns wired menuItemsConfig when enableDrillDown is true', () => {
const { result } = renderHook(() =>
usePanelContextMenu({
widget: mockWidget,
queryResponse: mockQueryResponse,
enableDrillDown: true,
}),
);
expect(result.current.menuItemsConfig).toStrictEqual({
header: 'menu-header',
items: 'menu-items',
});
});
it('clickHandlerWithContextMenu is a no-op when enableDrillDown is false', () => {
const { result } = renderHook(() =>
usePanelContextMenu({
widget: mockWidget,
queryResponse: mockQueryResponse,
enableDrillDown: false,
}),
);
result.current.clickHandlerWithContextMenu(
100, // xValue
200, // yValue
0, // mouseX
0, // mouseY
{ serviceName: 'svc' }, // metric
{ queryName: 'A', inFocusOrNot: true }, // queryData
10, // absoluteMouseX
20, // absoluteMouseY
{}, // axesData
{ seriesIndex: 0, seriesName: 'A', value: 1, color: '#abc' }, // focusedSeries
);
expect(onClickMock).not.toHaveBeenCalled();
});
it('clickHandlerWithContextMenu opens popover when enableDrillDown is true', () => {
const { result } = renderHook(() =>
usePanelContextMenu({
widget: mockWidget,
queryResponse: mockQueryResponse,
enableDrillDown: true,
}),
);
result.current.clickHandlerWithContextMenu(
100,
200,
0,
0,
{ serviceName: 'svc' },
{ queryName: 'A', inFocusOrNot: true },
10,
20,
{},
{ seriesIndex: 0, seriesName: 'A', value: 1, color: '#abc' },
);
expect(onClickMock).toHaveBeenCalledTimes(1);
});
it('defaults to disabled when enableDrillDown is not provided', () => {
const { result } = renderHook(() =>
usePanelContextMenu({
widget: mockWidget,
queryResponse: mockQueryResponse,
}),
);
expect(result.current.menuItemsConfig).toStrictEqual({});
});
});

View File

@@ -21,13 +21,11 @@ interface UseTimeSeriesContextMenuParams {
SuccessResponse<MetricRangePayloadProps, unknown>,
Error
>;
enableDrillDown?: boolean;
}
export const usePanelContextMenu = ({
widget,
queryResponse,
enableDrillDown = false,
}: UseTimeSeriesContextMenuParams): {
coordinates: { x: number; y: number } | null;
popoverPosition: PopoverPosition | null;
@@ -63,9 +61,6 @@ export const usePanelContextMenu = ({
const clickHandlerWithContextMenu = useCallback(
(...args: any[]) => {
if (!enableDrillDown) {
return;
}
const [
xValue,
_yvalue,
@@ -117,14 +112,14 @@ export const usePanelContextMenu = ({
});
}
},
[enableDrillDown, onClick, queryResponse],
[onClick, queryResponse],
);
return {
coordinates,
popoverPosition,
onClose,
menuItemsConfig: enableDrillDown ? menuItemsConfig : {},
menuItemsConfig,
clickHandlerWithContextMenu,
};
};

View File

@@ -31,7 +31,6 @@ function BarPanel(props: PanelWrapperProps): JSX.Element {
isFullViewMode,
onToggleModelHandler,
groupByPerQuery,
enableDrillDown = false,
} = props;
const uPlotRef = useRef<uPlot | null>(null);
const graphRef = useRef<HTMLDivElement>(null);
@@ -62,7 +61,6 @@ function BarPanel(props: PanelWrapperProps): JSX.Element {
} = usePanelContextMenu({
widget,
queryResponse,
enableDrillDown,
});
const config = useMemo(() => {

View File

@@ -31,7 +31,6 @@ function TimeSeriesPanel(props: PanelWrapperProps): JSX.Element {
isFullViewMode,
onToggleModelHandler,
groupByPerQuery,
enableDrillDown = false,
} = props;
const graphRef = useRef<HTMLDivElement>(null);
const [minTimeScale, setMinTimeScale] = useState<number>();
@@ -61,7 +60,6 @@ function TimeSeriesPanel(props: PanelWrapperProps): JSX.Element {
} = usePanelContextMenu({
widget,
queryResponse,
enableDrillDown,
});
const chartData = useMemo(() => {

View File

@@ -292,8 +292,6 @@ function FullView({
return <Spinner height="100%" size="large" tip="Loading..." />;
}
const showEditBtn = editWidget && dashboardEditView;
return (
<div className="full-view-container">
<OverlayScrollbar>
@@ -308,7 +306,7 @@ function FullView({
Reset Query
</Button>
)}
{showEditBtn && (
{editWidget && (
<Button
className="switch-edit-btn"
disabled={response.isFetching || response.isLoading}

View File

@@ -30,12 +30,7 @@ import { v4 as uuid } from 'uuid';
import { FeatureKeys } from '../../../constants/features';
import { useAppContext } from '../../../providers/App/App';
import {
GraphTitle,
MENU_ITEMS,
SERVICE_CHART_ID,
SERVICE_DETAIL_DRILLDOWN_ENABLED,
} from '../constant';
import { GraphTitle, MENU_ITEMS, SERVICE_CHART_ID } from '../constant';
import { getWidgetQueryBuilder } from '../MetricsApplication.factory';
import { Card, GraphContainer, Row } from '../styles';
import { Button } from './styles';
@@ -211,7 +206,6 @@ function DBCall(): JSX.Element {
}}
onDragSelect={onDragSelect}
version={ENTITY_VERSION_V4}
enableDrillDown={SERVICE_DETAIL_DRILLDOWN_ENABLED}
/>
</GraphContainer>
</Card>
@@ -250,7 +244,6 @@ function DBCall(): JSX.Element {
}}
onDragSelect={onDragSelect}
version={ENTITY_VERSION_V4}
enableDrillDown={SERVICE_DETAIL_DRILLDOWN_ENABLED}
/>
</GraphContainer>
</Card>

View File

@@ -32,12 +32,7 @@ import { v4 as uuid } from 'uuid';
import { FeatureKeys } from '../../../constants/features';
import { useAppContext } from '../../../providers/App/App';
import {
GraphTitle,
legend,
MENU_ITEMS,
SERVICE_DETAIL_DRILLDOWN_ENABLED,
} from '../constant';
import { GraphTitle, legend, MENU_ITEMS } from '../constant';
import { getWidgetQueryBuilder } from '../MetricsApplication.factory';
import { Card, GraphContainer, Row } from '../styles';
import GraphControlsPanel from './Overview/GraphControlsPanel/GraphControlsPanel';
@@ -284,7 +279,6 @@ function External(): JSX.Element {
}}
onDragSelect={onDragSelect}
version={ENTITY_VERSION_V4}
enableDrillDown={SERVICE_DETAIL_DRILLDOWN_ENABLED}
/>
</GraphContainer>
</Card>
@@ -328,7 +322,6 @@ function External(): JSX.Element {
}}
onDragSelect={onDragSelect}
version={ENTITY_VERSION_V4}
enableDrillDown={SERVICE_DETAIL_DRILLDOWN_ENABLED}
/>
</GraphContainer>
</Card>
@@ -373,7 +366,6 @@ function External(): JSX.Element {
}
onDragSelect={onDragSelect}
version={ENTITY_VERSION_V4}
enableDrillDown={SERVICE_DETAIL_DRILLDOWN_ENABLED}
/>
</GraphContainer>
</Card>
@@ -417,7 +409,6 @@ function External(): JSX.Element {
}}
onDragSelect={onDragSelect}
version={ENTITY_VERSION_V4}
enableDrillDown={SERVICE_DETAIL_DRILLDOWN_ENABLED}
/>
</GraphContainer>
</Card>

View File

@@ -15,7 +15,6 @@ import DisplayThreshold from 'container/GridCardLayout/WidgetHeader/DisplayThres
import {
GraphTitle,
SERVICE_CHART_ID,
SERVICE_DETAIL_DRILLDOWN_ENABLED,
} from 'container/MetricsApplication/constant';
import { getWidgetQueryBuilder } from 'container/MetricsApplication/MetricsApplication.factory';
import { apDexMetricsQueryBuilderQueries } from 'container/MetricsApplication/MetricsPageQueries/OverviewQueries';
@@ -106,7 +105,6 @@ function ApDexMetrics({
threshold={threshold}
isQueryEnabled={isQueryEnabled}
version={ENTITY_VERSION_V4}
enableDrillDown={SERVICE_DETAIL_DRILLDOWN_ENABLED}
/>
);
}

View File

@@ -8,7 +8,6 @@ import Graph from 'container/GridCardLayout/GridCard';
import {
GraphTitle,
SERVICE_CHART_ID,
SERVICE_DETAIL_DRILLDOWN_ENABLED,
} from 'container/MetricsApplication/constant';
import { getWidgetQueryBuilder } from 'container/MetricsApplication/MetricsApplication.factory';
import { latency } from 'container/MetricsApplication/MetricsPageQueries/OverviewQueries';
@@ -139,7 +138,6 @@ function ServiceOverview({
onClickHandler={handleGraphClick('Service')}
isQueryEnabled={isQueryEnabled}
version={ENTITY_VERSION_V4}
enableDrillDown={SERVICE_DETAIL_DRILLDOWN_ENABLED}
/>
)}
</GraphContainer>

View File

@@ -4,7 +4,6 @@ import axios from 'axios';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { ENTITY_VERSION_V4 } from 'constants/app';
import Graph from 'container/GridCardLayout/GridCard';
import { SERVICE_DETAIL_DRILLDOWN_ENABLED } from 'container/MetricsApplication/constant';
import { Card, GraphContainer } from 'container/MetricsApplication/styles';
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
import { Widgets } from 'types/api/dashboard/getAll';
@@ -44,7 +43,6 @@ function TopLevelOperation({
onDragSelect={onDragSelect}
isQueryEnabled={!topLevelOperationsIsLoading}
version={ENTITY_VERSION_V4}
enableDrillDown={SERVICE_DETAIL_DRILLDOWN_ENABLED}
/>
)}
</GraphContainer>

View File

@@ -25,8 +25,6 @@ export const OPERATION_LEGENDS = ['Operations'];
export const MENU_ITEMS = [MenuItemKeys.View, MenuItemKeys.CreateAlerts];
export const SERVICE_DETAIL_DRILLDOWN_ENABLED = true;
export enum FORMULA {
ERROR_PERCENTAGE = 'A*100/B',
DATABASE_CALLS_AVG_DURATION = 'A/B',

View File

@@ -169,10 +169,9 @@ describe('drilldownUtils', () => {
// Verify transformations were applied
if (filterExpression) {
// `operation` rewrites to `name` via source-side pass, then `name`
// is dropped by the logs target-side pass (logs has no span-name).
// Rule 2: operationname
expect(filterExpression).toContain(`name = 'GET'`);
expect(filterExpression).not.toContain(`operation = 'GET'`);
expect(filterExpression).not.toContain(`name = 'GET'`);
// Rule 3: span.kind → kind
expect(filterExpression).toContain(`${spanKindKey} = '${spanKindServer}'`);
@@ -263,9 +262,8 @@ describe('drilldownUtils', () => {
const filterExpression = result?.builder.queryData[0]?.filter?.expression;
if (filterExpression) {
// `operation` rewrites to `name` then drops for logs target.
expect(filterExpression).not.toContain(`operation = 'POST'`);
expect(filterExpression).not.toContain(`name = 'POST'`);
// All transformations should be applied
expect(filterExpression).toContain(`name = 'POST'`);
expect(filterExpression).toContain(`${spanKindKey} = '${spanKindClient}'`);
expect(filterExpression).toContain(`status_code_string = 'Error'`);
expect(filterExpression).toContain(`http.status_code = 500`);
@@ -412,9 +410,8 @@ describe('drilldownUtils', () => {
const filterExpression = result?.builder.queryData[0]?.filter?.expression;
if (filterExpression) {
// `operation` rewrites to `name` then drops for logs target.
expect(filterExpression).not.toContain(`operation = 'GET'`);
expect(filterExpression).not.toContain(`name = 'GET'`);
// Transformed attributes
expect(filterExpression).toContain(`name = 'GET'`);
expect(filterExpression).toContain(`${spanKindKey} = '${spanKindServer}'`);
// Preserved non-metric attributes
@@ -502,189 +499,4 @@ describe('drilldownUtils', () => {
});
});
});
describe('getViewQuery target-aware sanitisation (serviceName / name)', () => {
const makeQuery = (
expression: string,
dataSource: 'traces' | 'logs' | 'metrics' = 'traces',
): Query => ({
id: 'src-query',
queryType: 'builder' as any,
builder: {
queryData: [
{
queryName: 'src',
dataSource: dataSource as any,
aggregations: [{ metricName: 'non_apm_metric' }] as any,
groupBy: [],
expression: '',
disabled: false,
functions: [],
legend: '',
having: [],
limit: null,
stepInterval: undefined,
orderBy: [],
filter: { expression },
},
],
queryFormulas: [],
queryTraceOperator: [],
},
promql: [],
clickhouse_sql: [],
});
it('rewrites serviceName -> service.name when drilling to logs', () => {
const result = getViewQuery(
makeQuery(`serviceName = 'svc'`),
[],
'view_logs',
'src',
);
const expr = result?.builder.queryData[0]?.filter?.expression || '';
expect(expr).toContain(`service.name = 'svc'`);
expect(expr).not.toContain('serviceName');
});
it('rewrites serviceName -> service.name when drilling to traces', () => {
const result = getViewQuery(
makeQuery(`serviceName = 'svc'`),
[],
'view_traces',
'src',
);
const expr = result?.builder.queryData[0]?.filter?.expression || '';
expect(expr).toContain(`service.name = 'svc'`);
expect(expr).not.toContain('serviceName');
});
it('drops `name` clause when drilling to logs', () => {
const result = getViewQuery(
makeQuery(`name = 'GET /api'`),
[],
'view_logs',
'src',
);
const expr = result?.builder.queryData[0]?.filter?.expression || '';
expect(expr).not.toContain(`name = 'GET /api'`);
});
it('keeps `name` clause when drilling to traces', () => {
const result = getViewQuery(
makeQuery(`name = 'GET /api'`),
[],
'view_traces',
'src',
);
const expr = result?.builder.queryData[0]?.filter?.expression || '';
expect(expr).toContain(`name = 'GET /api'`);
});
it('combined: drilling to logs rewrites serviceName and drops name', () => {
const result = getViewQuery(
makeQuery(`serviceName = 'svc' AND name = 'GET /api'`),
[],
'view_logs',
'src',
);
const expr = result?.builder.queryData[0]?.filter?.expression || '';
expect(expr).toContain(`service.name = 'svc'`);
expect(expr).not.toContain('serviceName');
expect(expr).not.toContain(`name = 'GET /api'`);
});
it('combined: drilling to traces rewrites serviceName and keeps name', () => {
const result = getViewQuery(
makeQuery(`serviceName = 'svc' AND name = 'GET /api'`),
[],
'view_traces',
'src',
);
const expr = result?.builder.queryData[0]?.filter?.expression || '';
expect(expr).toContain(`service.name = 'svc'`);
expect(expr).toContain(`name = 'GET /api'`);
expect(expr).not.toContain('serviceName');
});
it('metric-APM source -> traces target preserves existing operation -> name rewrite', () => {
const metricsQuery: Query = {
id: 'apm-metrics',
queryType: 'builder' as any,
builder: {
queryData: [
{
queryName: 'm',
dataSource: 'metrics' as any,
aggregations: [{ metricName: 'signoz_calls_total' }] as any,
groupBy: [],
expression: '',
disabled: false,
functions: [],
legend: '',
having: [],
limit: null,
stepInterval: undefined,
orderBy: [],
filter: { expression: `operation = 'GET'` },
},
],
queryFormulas: [],
queryTraceOperator: [],
},
promql: [],
clickhouse_sql: [],
};
const result = getViewQuery(metricsQuery, [], 'view_traces', 'm');
const expr = result?.builder.queryData[0]?.filter?.expression || '';
expect(expr).toContain(`name = 'GET'`);
expect(expr).not.toContain(`operation = 'GET'`);
});
it('metric-APM source -> logs target: operation rewrites to name, then dropped', () => {
const metricsQuery: Query = {
id: 'apm-metrics',
queryType: 'builder' as any,
builder: {
queryData: [
{
queryName: 'm',
dataSource: 'metrics' as any,
aggregations: [{ metricName: 'signoz_calls_total' }] as any,
groupBy: [],
expression: '',
disabled: false,
functions: [],
legend: '',
having: [],
limit: null,
stepInterval: undefined,
orderBy: [],
filter: { expression: `operation = 'GET'` },
},
],
queryFormulas: [],
queryTraceOperator: [],
},
promql: [],
clickhouse_sql: [],
};
const result = getViewQuery(metricsQuery, [], 'view_logs', 'm');
const expr = result?.builder.queryData[0]?.filter?.expression || '';
expect(expr).not.toContain(`operation = 'GET'`);
expect(expr).not.toContain(`name = 'GET'`);
});
it('drilling to metrics does not apply target-side sanitisation', () => {
const result = getViewQuery(
makeQuery(`serviceName = 'svc' AND name = 'GET /api'`),
[],
'view_metrics',
'src',
);
const expr = result?.builder.queryData[0]?.filter?.expression || '';
expect(expr).toContain('serviceName');
expect(expr).toContain(`name = 'GET /api'`);
});
});
});

View File

@@ -7,10 +7,8 @@ import {
import ROUTES from 'constants/routes';
import { isApmMetric } from 'container/PanelWrapper/utils';
import {
applyMappingsToExpression,
DRILLDOWN_TO_LOGS_MAPPINGS,
DRILLDOWN_TO_TRACES_MAPPINGS,
METRIC_TO_LOGS_TRACES_MAPPINGS,
replaceKeysAndValuesInExpression,
} from 'container/QueryTable/Drilldown/metricsCorrelationUtils';
import cloneDeep from 'lodash-es/cloneDeep';
import {
@@ -349,41 +347,27 @@ export const getViewQuery = (
newQuery.builder.queryData[0].filter = newFilterExpression;
try {
// Drill-down filter sanitisation. Two stages:
// 1. Source-side: rewrite metric-APM-specific keys (operation, span.kind,
// status.code) so they map onto trace/log columns.
// 2. Target-side: normalise legacy keys to OTel-canonical (`serviceName`
// -> `service.name`) and drop keys with no equivalent in the target
// datasource (e.g. `name` for logs).
let expression = newFilterExpression?.expression || '';
// ===========================================
// TEMP LOGIC - TO BE REMOVED LATER
// ===========================================
// Apply metric-to-logs/traces transformations
const specificQuery = getQueryData(query, queryName);
const isMetricQuery = specificQuery?.dataSource === 'metrics';
const metricName = (specificQuery?.aggregations?.[0] as MetricAggregation)
?.metricName;
if (isMetricQuery && isApmMetric(metricName || '')) {
expression = applyMappingsToExpression(
expression,
const transformedExpression = replaceKeysAndValuesInExpression(
newFilterExpression?.expression || '',
METRIC_TO_LOGS_TRACES_MAPPINGS,
);
newQuery.builder.queryData[0].filter = {
expression: transformedExpression || '',
};
}
if (key === 'view_logs') {
expression = applyMappingsToExpression(
expression,
DRILLDOWN_TO_LOGS_MAPPINGS,
);
} else if (key === 'view_traces') {
expression = applyMappingsToExpression(
expression,
DRILLDOWN_TO_TRACES_MAPPINGS,
);
}
newQuery.builder.queryData[0].filter = { expression };
// ===========================================
} catch (error) {
console.error('Error sanitising drilldown filter expression:', error);
console.error('Error transforming metrics to logs/traces:', error);
}
return newQuery;

View File

@@ -1,8 +1,5 @@
/* eslint-disable sonarjs/cognitive-complexity */
import {
formatValueForExpression,
removeKeysFromExpression,
} from 'components/QueryBuilderV2/utils';
import { formatValueForExpression } from 'components/QueryBuilderV2/utils';
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
import { IQueryPair } from 'types/antlrQueryTypes';
import { extractQueryPairs } from 'utils/queryContextUtils';
@@ -11,7 +8,7 @@ import { isFunctionOperator, isNonValueOperator } from 'utils/tokenUtils';
type KeyValueMapping = {
attribute: string;
newAttribute: string | null;
newAttribute: string;
valueMappings: Record<string, string>;
};
@@ -43,33 +40,8 @@ export const METRIC_TO_LOGS_TRACES_MAPPINGS: KeyValueMapping[] = [
},
];
export const DRILLDOWN_TO_LOGS_MAPPINGS: KeyValueMapping[] = [
{
attribute: 'serviceName',
newAttribute: 'service.name',
valueMappings: {},
},
{
attribute: 'name',
newAttribute: null,
valueMappings: {},
},
];
export const DRILLDOWN_TO_TRACES_MAPPINGS: KeyValueMapping[] = [
{
attribute: 'serviceName',
newAttribute: 'service.name',
valueMappings: {},
},
];
// Logic for rewriting key/values in an expression using provided mappings.
// Callers must pre-filter mappings to ensure newAttribute is non-null.
function modifyKeyVal(
pair: IQueryPair,
mapping: KeyValueMapping & { newAttribute: string },
): string {
function modifyKeyVal(pair: IQueryPair, mapping: KeyValueMapping): string {
const newKey = mapping.newAttribute;
const op = pair.operator;
@@ -135,18 +107,8 @@ export function replaceKeysAndValuesInExpression(
return expression;
}
// Only rewrite mappings (newAttribute non-null) are processed here.
// Drops are handled separately by applyMappingsToExpression via removeKeysFromExpression.
const attributeToMapping = new Map<
string,
KeyValueMapping & { newAttribute: string }
>(
mappingList
.filter(
(m): m is KeyValueMapping & { newAttribute: string } =>
m.newAttribute !== null,
)
.map((m) => [m.attribute.trim().toLowerCase(), m]),
const attributeToMapping = new Map<string, KeyValueMapping>(
mappingList.map((m) => [m.attribute.trim().toLowerCase(), m]),
);
const pairs: IQueryPair[] = extractQueryPairs(expression);
@@ -217,26 +179,3 @@ export function replaceKeysAndValuesInExpression(
return resultParts.join('');
}
// Apply a list of mappings to a filter expression. Rewrites are applied first
// (newAttribute is a string), then drops (newAttribute is null) via the
// ANTLR-parser-based removeKeysFromExpression which handles AND/OR/NOT/paren
// elision correctly.
export function applyMappingsToExpression(
expression: string,
mappings: KeyValueMapping[],
): string {
if (!expression || !mappings || mappings.length === 0) {
return expression;
}
const dropKeys = mappings
.filter((m) => m.newAttribute === null)
.map((m) => m.attribute);
let result = replaceKeysAndValuesInExpression(expression, mappings);
if (dropKeys.length > 0) {
result = removeKeysFromExpression(result, dropKeys);
}
return result;
}

View File

@@ -119,12 +119,6 @@
flex-shrink: 0;
}
.statusMessageBadge {
width: 100%;
min-width: 0;
box-sizing: border-box;
}
.traceId {
color: var(--accent-primary);
overflow: hidden;

View File

@@ -1,6 +1,5 @@
import { ReactNode } from 'react';
import { Badge } from '@signozhq/ui/badge';
import ExpandableValue from 'periscope/components/ExpandableValue';
import { SpanV3 } from 'types/api/trace/getTraceV3';
import styles from './SpanDetailsPanel.module.scss';
@@ -49,15 +48,7 @@ export const HIGHLIGHTED_OPTIONS: HighlightedOption[] = [
label: 'STATUS MESSAGE',
render: (span): ReactNode | null =>
span.status_message ? (
<ExpandableValue value={span.status_message} title="Status message">
<Badge
color="vanilla"
textEllipsis="end"
className={styles.statusMessageBadge}
>
{span.status_message}
</Badge>
</ExpandableValue>
<Badge color="vanilla">{span.status_message}</Badge>
) : null,
},
];

View File

@@ -1,3 +0,0 @@
.traceOptionsDropdown {
z-index: 1100;
}

View File

@@ -6,8 +6,6 @@ import { Ellipsis } from '@signozhq/icons';
import { useTraceStore } from '../stores/traceStore';
import styles from './TraceOptionsMenu.module.scss';
interface TraceOptionsMenuProps {
showTraceDetails: boolean;
onToggleTraceDetails: () => void;
@@ -84,11 +82,7 @@ function TraceOptionsMenu({
]);
return (
<Dropdown
menu={{ items: menuItems }}
align="start"
className={styles.traceOptionsDropdown}
>
<Dropdown menu={{ items: menuItems }} align="start">
<Button
variant="ghost"
size="icon"

View File

@@ -1,49 +0,0 @@
.trigger {
display: block;
min-width: 0;
max-width: 100%;
overflow: hidden;
[data-truncated='true'] {
pointer-events: none;
}
}
.tooltipContent {
display: flex;
flex-direction: column;
gap: 8px;
max-width: 480px;
padding: 8px;
}
.preview {
margin: 0;
max-height: 220px;
overflow: auto;
white-space: pre-wrap;
word-break: break-word;
font-family: var(--font-family-mono, monospace);
font-size: 12px;
line-height: 1.4;
}
.expandButton {
align-self: flex-end;
}
.dialog {
max-width: 80vw;
width: 80vw;
}
.fullValue {
margin: 0;
max-height: 70vh;
overflow: auto;
white-space: pre-wrap;
word-break: break-word;
font-family: var(--font-family-mono, monospace);
font-size: 13px;
line-height: 1.5;
}

View File

@@ -1,78 +0,0 @@
import { ReactNode, useState } from 'react';
import { Button } from '@signozhq/ui/button';
import { DialogWrapper } from '@signozhq/ui/dialog';
import {
TooltipContent,
TooltipProvider,
TooltipRoot,
TooltipTrigger,
} from '@signozhq/ui/tooltip';
import { Fullscreen } from '@signozhq/icons';
import styles from './ExpandableValue.module.scss';
const DEFAULT_THRESHOLD = 100;
const DEFAULT_DIALOG_TITLE = 'Value';
const DEFAULT_Z_INDEX = 1100;
interface ExpandableValueProps {
value: string;
title?: string;
threshold?: number;
zIndex?: number;
children: ReactNode;
}
function ExpandableValue({
value,
title = DEFAULT_DIALOG_TITLE,
threshold = DEFAULT_THRESHOLD,
zIndex = DEFAULT_Z_INDEX,
children,
}: ExpandableValueProps): JSX.Element {
const [isDialogOpen, setIsDialogOpen] = useState(false);
if (value.length <= threshold) {
return <>{children}</>;
}
return (
<TooltipProvider>
<TooltipRoot>
<TooltipTrigger asChild>
<span className={styles.trigger}>{children}</span>
</TooltipTrigger>
<TooltipContent
className={styles.tooltipContent}
side="top"
style={{ zIndex }}
>
<pre className={styles.preview}>{value}</pre>
<Button
variant="outlined"
color="secondary"
size="sm"
prefix={<Fullscreen size={14} />}
onClick={(): void => setIsDialogOpen(true)}
className={styles.expandButton}
>
Expand
</Button>
</TooltipContent>
</TooltipRoot>
<DialogWrapper
title={title}
open={isDialogOpen}
onOpenChange={setIsDialogOpen}
className={styles.dialog}
style={{ zIndex }}
>
<pre className={styles.fullValue}>{value}</pre>
</DialogWrapper>
</TooltipProvider>
);
}
export default ExpandableValue;

View File

@@ -1 +0,0 @@
export { default } from './ExpandableValue';

View File

@@ -26,7 +26,7 @@ func buildClusterRecords(
records := make([]inframonitoringtypes.ClusterRecord, 0, len(pageGroups))
for _, labels := range pageGroups {
compositeKey := compositeKeyFromLabels(labels, groupBy)
clusterName := labels[inframonitoringtypes.ClusterNameAttrKey]
clusterName := labels[clusterNameAttrKey]
record := inframonitoringtypes.ClusterRecord{ // initialize with default values
ClusterName: clusterName,
@@ -87,9 +87,6 @@ func (m *module) getTopClusterGroups(
metadataMap map[string]map[string]string,
) ([]map[string]string, error) {
orderByKey := req.OrderBy.Key.Name
if orderByKey == inframonitoringtypes.ClusterNameAttrKey {
return inframonitoringtypes.PaginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.ClusterNameAttrKey), nil
}
queryNamesForOrderBy := orderByToClustersQueryNames[orderByKey]
rankingQueryName := queryNamesForOrderBy[len(queryNamesForOrderBy)-1]

View File

@@ -7,9 +7,14 @@ import (
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
)
// TODO(nikhilmantri0902): change to k8s.cluster.uid after showing the missing
// data banner. Carried forward from v1 (see k8sClusterUIDAttrKey in
// pkg/query-service/app/inframetrics/clusters.go).
const clusterNameAttrKey = "k8s.cluster.name"
var clusterNameGroupByKey = qbtypes.GroupByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: inframonitoringtypes.ClusterNameAttrKey,
Name: clusterNameAttrKey,
FieldContext: telemetrytypes.FieldContextResource,
FieldDataType: telemetrytypes.FieldDataTypeString,
},

View File

@@ -25,7 +25,7 @@ func buildDaemonSetRecords(
records := make([]inframonitoringtypes.DaemonSetRecord, 0, len(pageGroups))
for _, labels := range pageGroups {
compositeKey := compositeKeyFromLabels(labels, groupBy)
daemonSetName := labels[inframonitoringtypes.DaemonSetNameAttrKey]
daemonSetName := labels[daemonSetNameAttrKey]
record := inframonitoringtypes.DaemonSetRecord{ // initialize with default values
DaemonSetName: daemonSetName,
@@ -95,9 +95,6 @@ func (m *module) getTopDaemonSetGroups(
metadataMap map[string]map[string]string,
) ([]map[string]string, error) {
orderByKey := req.OrderBy.Key.Name
if orderByKey == inframonitoringtypes.DaemonSetNameAttrKey {
return inframonitoringtypes.PaginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.DaemonSetNameAttrKey), nil
}
queryNamesForOrderBy := orderByToDaemonSetsQueryNames[orderByKey]
rankingQueryName := queryNamesForOrderBy[len(queryNamesForOrderBy)-1]

View File

@@ -7,11 +7,14 @@ import (
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
)
const daemonSetsBaseFilterExpr = "k8s.daemonset.name != ''"
const (
daemonSetNameAttrKey = "k8s.daemonset.name"
daemonSetsBaseFilterExpr = "k8s.daemonset.name != ''"
)
var daemonSetNameGroupByKey = qbtypes.GroupByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: inframonitoringtypes.DaemonSetNameAttrKey,
Name: daemonSetNameAttrKey,
FieldContext: telemetrytypes.FieldContextResource,
FieldDataType: telemetrytypes.FieldDataTypeString,
},

View File

@@ -25,7 +25,7 @@ func buildDeploymentRecords(
records := make([]inframonitoringtypes.DeploymentRecord, 0, len(pageGroups))
for _, labels := range pageGroups {
compositeKey := compositeKeyFromLabels(labels, groupBy)
deploymentName := labels[inframonitoringtypes.DeploymentNameAttrKey]
deploymentName := labels[deploymentNameAttrKey]
record := inframonitoringtypes.DeploymentRecord{ // initialize with default values
DeploymentName: deploymentName,
@@ -95,9 +95,6 @@ func (m *module) getTopDeploymentGroups(
metadataMap map[string]map[string]string,
) ([]map[string]string, error) {
orderByKey := req.OrderBy.Key.Name
if orderByKey == inframonitoringtypes.DeploymentNameAttrKey {
return inframonitoringtypes.PaginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.DeploymentNameAttrKey), nil
}
queryNamesForOrderBy := orderByToDeploymentsQueryNames[orderByKey]
rankingQueryName := queryNamesForOrderBy[len(queryNamesForOrderBy)-1]

View File

@@ -7,11 +7,14 @@ import (
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
)
const deploymentsBaseFilterExpr = "k8s.deployment.name != ''"
const (
deploymentNameAttrKey = "k8s.deployment.name"
deploymentsBaseFilterExpr = "k8s.deployment.name != ''"
)
var deploymentNameGroupByKey = qbtypes.GroupByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: inframonitoringtypes.DeploymentNameAttrKey,
Name: deploymentNameAttrKey,
FieldContext: telemetrytypes.FieldContextResource,
FieldDataType: telemetrytypes.FieldDataTypeString,
},

View File

@@ -38,7 +38,7 @@ func (m *module) getPerGroupHostStatusCounts(
uint64(req.Start), uint64(req.End), nil,
)
hostNameExpr := fmt.Sprintf("JSONExtractString(labels, '%s')", inframonitoringtypes.HostNameAttrKey)
hostNameExpr := fmt.Sprintf("JSONExtractString(labels, '%s')", hostNameAttrKey)
sb := sqlbuilder.NewSelectBuilder()
selectCols := make([]string, 0, len(req.GroupBy)+2)
@@ -48,7 +48,7 @@ func (m *module) getPerGroupHostStatusCounts(
)
}
activeHostsSQ := m.getActiveHostsQuery(metricNames, inframonitoringtypes.HostNameAttrKey, sinceUnixMilli)
activeHostsSQ := m.getActiveHostsQuery(metricNames, hostNameAttrKey, sinceUnixMilli)
selectCols = append(selectCols,
fmt.Sprintf("uniqExactIf(%s, %s GLOBAL IN (%s)) AS active_host_count", hostNameExpr, hostNameExpr, sb.Var(activeHostsSQ)),
fmt.Sprintf("uniqExactIf(%s, %s != '') AS total_host_count", hostNameExpr, hostNameExpr),
@@ -142,7 +142,7 @@ func buildHostRecords(
records := make([]inframonitoringtypes.HostRecord, 0, len(pageGroups))
for _, labels := range pageGroups {
compositeKey := compositeKeyFromLabels(labels, groupBy)
hostName := labels[inframonitoringtypes.HostNameAttrKey]
hostName := labels[hostNameAttrKey]
activeStatus := inframonitoringtypes.HostStatusNone
activeHostCount := 0
@@ -216,9 +216,6 @@ func (m *module) getTopHostGroups(
metadataMap map[string]map[string]string,
) ([]map[string]string, error) {
orderByKey := req.OrderBy.Key.Name
if orderByKey == inframonitoringtypes.HostNameAttrKey {
return inframonitoringtypes.PaginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.HostNameAttrKey), nil
}
queryNamesForOrderBy := orderByToHostsQueryNames[orderByKey]
// The last entry is the formula/query whose value we sort by.
rankingQueryName := queryNamesForOrderBy[len(queryNamesForOrderBy)-1]
@@ -284,7 +281,7 @@ func (m *module) applyHostsActiveStatusFilter(req *inframonitoringtypes.Postable
if req.Filter.FilterByStatus == inframonitoringtypes.HostStatusInactive {
op = "NOT IN"
}
statusClause := fmt.Sprintf("%s %s (%s)", inframonitoringtypes.HostNameAttrKey, op, strings.Join(activeHosts, ", "))
statusClause := fmt.Sprintf("%s %s (%s)", hostNameAttrKey, op, strings.Join(activeHosts, ", "))
req.Filter.Expression = mergeFilterExpressions(req.Filter.Expression, statusClause)
return false
}

View File

@@ -7,10 +7,14 @@ import (
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
)
const (
hostNameAttrKey = "host.name"
)
// Helper group-by key used across all queries.
var hostNameGroupByKey = qbtypes.GroupByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: inframonitoringtypes.HostNameAttrKey,
Name: hostNameAttrKey,
FieldContext: telemetrytypes.FieldContextResource,
FieldDataType: telemetrytypes.FieldDataTypeString,
},

View File

@@ -25,7 +25,7 @@ func buildJobRecords(
records := make([]inframonitoringtypes.JobRecord, 0, len(pageGroups))
for _, labels := range pageGroups {
compositeKey := compositeKeyFromLabels(labels, groupBy)
jobName := labels[inframonitoringtypes.JobNameAttrKey]
jobName := labels[jobNameAttrKey]
record := inframonitoringtypes.JobRecord{ // initialize with default values
JobName: jobName,
@@ -103,9 +103,6 @@ func (m *module) getTopJobGroups(
metadataMap map[string]map[string]string,
) ([]map[string]string, error) {
orderByKey := req.OrderBy.Key.Name
if orderByKey == inframonitoringtypes.JobNameAttrKey {
return inframonitoringtypes.PaginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.JobNameAttrKey), nil
}
queryNamesForOrderBy := orderByToJobsQueryNames[orderByKey]
rankingQueryName := queryNamesForOrderBy[len(queryNamesForOrderBy)-1]

View File

@@ -7,11 +7,14 @@ import (
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
)
const jobsBaseFilterExpr = "k8s.job.name != ''"
const (
jobNameAttrKey = "k8s.job.name"
jobsBaseFilterExpr = "k8s.job.name != ''"
)
var jobNameGroupByKey = qbtypes.GroupByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: inframonitoringtypes.JobNameAttrKey,
Name: jobNameAttrKey,
FieldContext: telemetrytypes.FieldContextResource,
FieldDataType: telemetrytypes.FieldDataTypeString,
},

View File

@@ -100,7 +100,7 @@ func (m *module) ListHosts(ctx context.Context, orgID valuer.UUID, req *inframon
// Determine active hosts: those with metrics reported in the last 10 minutes.
// Compute the cutoff once so every downstream query/subquery agrees on what "active" means.
sinceUnixMilli := time.Now().Add(-10 * time.Minute).UTC().UnixMilli()
activeHostsMap, err := m.getActiveHosts(ctx, hostsTableMetricNamesList, inframonitoringtypes.HostNameAttrKey, sinceUnixMilli)
activeHostsMap, err := m.getActiveHosts(ctx, hostsTableMetricNamesList, hostNameAttrKey, sinceUnixMilli)
if err != nil {
return nil, err
}
@@ -146,7 +146,7 @@ func (m *module) ListHosts(ctx context.Context, orgID valuer.UUID, req *inframon
// When host.name is not in groupBy, we need to run an additional query to get the counts per group for the current page,
// using the same filter expression as the main query (including user filters + page groups IN clause).
hostCounts := make(map[string]groupHostStatusCounts)
isHostNameInGroupBy := isKeyInGroupByAttrs(req.GroupBy, inframonitoringtypes.HostNameAttrKey)
isHostNameInGroupBy := isKeyInGroupByAttrs(req.GroupBy, hostNameAttrKey)
if !isHostNameInGroupBy {
hostCounts, err = m.getPerGroupHostStatusCounts(ctx, req, hostsTableMetricNamesList, pageGroups, sinceUnixMilli)
if err != nil {
@@ -324,7 +324,7 @@ func (m *module) ListNodes(ctx context.Context, orgID valuer.UUID, req *inframon
return nil, err
}
isNodeNameInGroupBy := isKeyInGroupByAttrs(req.GroupBy, inframonitoringtypes.NodeNameAttrKey)
isNodeNameInGroupBy := isKeyInGroupByAttrs(req.GroupBy, nodeNameAttrKey)
resp.Records = buildNodeRecords(isNodeNameInGroupBy, queryResp, pageGroups, req.GroupBy, metadataMap, nodeConditionCounts, podPhaseCounts)
resp.Warning = queryResp.Warning

View File

@@ -24,7 +24,7 @@ func buildNamespaceRecords(
records := make([]inframonitoringtypes.NamespaceRecord, 0, len(pageGroups))
for _, labels := range pageGroups {
compositeKey := compositeKeyFromLabels(labels, groupBy)
namespaceName := labels[inframonitoringtypes.NamespaceNameAttrKey]
namespaceName := labels[namespaceNameAttrKey]
record := inframonitoringtypes.NamespaceRecord{ // initialize with default values
NamespaceName: namespaceName,
@@ -70,9 +70,6 @@ func (m *module) getTopNamespaceGroups(
metadataMap map[string]map[string]string,
) ([]map[string]string, error) {
orderByKey := req.OrderBy.Key.Name
if orderByKey == inframonitoringtypes.NamespaceNameAttrKey {
return inframonitoringtypes.PaginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.NamespaceNameAttrKey), nil
}
queryNamesForOrderBy := orderByToNamespacesQueryNames[orderByKey]
rankingQueryName := queryNamesForOrderBy[len(queryNamesForOrderBy)-1]

View File

@@ -7,9 +7,13 @@ import (
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
)
const (
namespaceNameAttrKey = "k8s.namespace.name"
)
var namespaceNameGroupByKey = qbtypes.GroupByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: inframonitoringtypes.NamespaceNameAttrKey,
Name: namespaceNameAttrKey,
FieldContext: telemetrytypes.FieldContextResource,
FieldDataType: telemetrytypes.FieldDataTypeString,
},

View File

@@ -33,7 +33,7 @@ func buildNodeRecords(
records := make([]inframonitoringtypes.NodeRecord, 0, len(pageGroups))
for _, labels := range pageGroups {
compositeKey := compositeKeyFromLabels(labels, groupBy)
nodeName := labels[inframonitoringtypes.NodeNameAttrKey]
nodeName := labels[nodeNameAttrKey]
record := inframonitoringtypes.NodeRecord{ // initialize with default values
NodeName: nodeName,
@@ -105,9 +105,6 @@ func (m *module) getTopNodeGroups(
metadataMap map[string]map[string]string,
) ([]map[string]string, error) {
orderByKey := req.OrderBy.Key.Name
if orderByKey == inframonitoringtypes.NodeNameAttrKey {
return inframonitoringtypes.PaginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.NodeNameAttrKey), nil
}
queryNamesForOrderBy := orderByToNodesQueryNames[orderByKey]
rankingQueryName := queryNamesForOrderBy[len(queryNamesForOrderBy)-1]
@@ -204,7 +201,7 @@ func (m *module) getPerGroupNodeConditionCounts(
timeSeriesFPs := sqlbuilder.NewSelectBuilder()
timeSeriesFPsSelectCols := []string{
"fingerprint",
fmt.Sprintf("JSONExtractString(labels, %s) AS node_name", timeSeriesFPs.Var(inframonitoringtypes.NodeNameAttrKey)),
fmt.Sprintf("JSONExtractString(labels, %s) AS node_name", timeSeriesFPs.Var(nodeNameAttrKey)),
}
for _, key := range groupBy {
timeSeriesFPsSelectCols = append(timeSeriesFPsSelectCols,

View File

@@ -7,11 +7,14 @@ import (
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
)
const nodeConditionMetricName = "k8s.node.condition_ready"
const (
nodeNameAttrKey = "k8s.node.name"
nodeConditionMetricName = "k8s.node.condition_ready"
)
var nodeNameGroupByKey = qbtypes.GroupByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: inframonitoringtypes.NodeNameAttrKey,
Name: nodeNameAttrKey,
FieldContext: telemetrytypes.FieldContextResource,
FieldDataType: telemetrytypes.FieldDataTypeString,
},

View File

@@ -124,9 +124,6 @@ func (m *module) getTopPodGroups(
metadataMap map[string]map[string]string,
) ([]map[string]string, error) {
orderByKey := req.OrderBy.Key.Name
if orderByKey == inframonitoringtypes.PodNameAttrKey {
return inframonitoringtypes.PaginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.PodNameAttrKey), nil
}
queryNamesForOrderBy := orderByToPodsQueryNames[orderByKey]
rankingQueryName := queryNamesForOrderBy[len(queryNamesForOrderBy)-1]

View File

@@ -25,7 +25,7 @@ func buildStatefulSetRecords(
records := make([]inframonitoringtypes.StatefulSetRecord, 0, len(pageGroups))
for _, labels := range pageGroups {
compositeKey := compositeKeyFromLabels(labels, groupBy)
statefulSetName := labels[inframonitoringtypes.StatefulSetNameAttrKey]
statefulSetName := labels[statefulSetNameAttrKey]
record := inframonitoringtypes.StatefulSetRecord{ // initialize with default values
StatefulSetName: statefulSetName,
@@ -95,9 +95,6 @@ func (m *module) getTopStatefulSetGroups(
metadataMap map[string]map[string]string,
) ([]map[string]string, error) {
orderByKey := req.OrderBy.Key.Name
if orderByKey == inframonitoringtypes.StatefulSetNameAttrKey {
return inframonitoringtypes.PaginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.StatefulSetNameAttrKey), nil
}
queryNamesForOrderBy := orderByToStatefulSetsQueryNames[orderByKey]
rankingQueryName := queryNamesForOrderBy[len(queryNamesForOrderBy)-1]

View File

@@ -7,11 +7,14 @@ import (
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
)
const statefulSetsBaseFilterExpr = "k8s.statefulset.name != ''"
const (
statefulSetNameAttrKey = "k8s.statefulset.name"
statefulSetsBaseFilterExpr = "k8s.statefulset.name != ''"
)
var statefulSetNameGroupByKey = qbtypes.GroupByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: inframonitoringtypes.StatefulSetNameAttrKey,
Name: statefulSetNameAttrKey,
FieldContext: telemetrytypes.FieldContextResource,
FieldDataType: telemetrytypes.FieldDataTypeString,
},

View File

@@ -23,7 +23,7 @@ func buildVolumeRecords(
records := make([]inframonitoringtypes.VolumeRecord, 0, len(pageGroups))
for _, labels := range pageGroups {
compositeKey := compositeKeyFromLabels(labels, groupBy)
pvcName := labels[inframonitoringtypes.PersistentVolumeClaimNameAttrKey]
pvcName := labels[persistentVolumeClaimNameAttrKey]
record := inframonitoringtypes.VolumeRecord{ // initialize with default values
PersistentVolumeClaimName: pvcName,
@@ -75,9 +75,6 @@ func (m *module) getTopVolumeGroups(
metadataMap map[string]map[string]string,
) ([]map[string]string, error) {
orderByKey := req.OrderBy.Key.Name
if orderByKey == inframonitoringtypes.PersistentVolumeClaimNameAttrKey {
return inframonitoringtypes.PaginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.PersistentVolumeClaimNameAttrKey), nil
}
queryNamesForOrderBy := orderByToVolumesQueryNames[orderByKey]
rankingQueryName := queryNamesForOrderBy[len(queryNamesForOrderBy)-1]

View File

@@ -7,11 +7,14 @@ import (
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
)
const volumesBaseFilterExpr = "k8s.persistentvolumeclaim.name != ''"
const (
persistentVolumeClaimNameAttrKey = "k8s.persistentvolumeclaim.name"
volumesBaseFilterExpr = "k8s.persistentvolumeclaim.name != ''"
)
var pvcNameGroupByKey = qbtypes.GroupByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: inframonitoringtypes.PersistentVolumeClaimNameAttrKey,
Name: persistentVolumeClaimNameAttrKey,
FieldContext: telemetrytypes.FieldContextResource,
FieldDataType: telemetrytypes.FieldDataTypeString,
},

View File

@@ -74,7 +74,7 @@ func (h *provider) BeforeQuery(ctx context.Context, _ *telemetrystore.QueryEvent
// TODO(srikanthccv): enable it when the "Cannot read all data" issue is fixed
// https://github.com/ClickHouse/ClickHouse/issues/82283
settings["secondary_indices_enable_bulk_filtering"] = false
settings["secondary_indices_enable_bulk_filtering"] = true
ctx = clickhouse.Context(ctx, clickhouse.WithSettings(settings))
return ctx

View File

@@ -19,8 +19,8 @@ type Clusters struct {
type ClusterRecord struct {
// TODO(nikhilmantri0902): once the underlying attr key is migrated to
// k8s.cluster.uid (see ClusterNameAttrKey), surface ClusterUID alongside
// (or replace) ClusterName.
// k8s.cluster.uid (see clusterNameAttrKey TODO in implinframonitoring),
// surface ClusterUID alongside (or replace) ClusterName.
ClusterName string `json:"clusterName" required:"true"`
ClusterCPU float64 `json:"clusterCPU" required:"true"`
ClusterCPUAllocatable float64 `json:"clusterCPUAllocatable" required:"true"`
@@ -88,9 +88,6 @@ func (req *PostableClusters) Validate() error {
if req.OrderBy.Direction != qbtypes.OrderDirectionAsc && req.OrderBy.Direction != qbtypes.OrderDirectionDesc {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid order by direction: %s", req.OrderBy.Direction)
}
if req.OrderBy.Key.Name == ClusterNameAttrKey && len(req.GroupBy) > 0 {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "order by '%s' is only allowed when groupBy is empty", ClusterNameAttrKey)
}
}
return nil

View File

@@ -1,7 +1,5 @@
package inframonitoringtypes
const ClusterNameAttrKey = "k8s.cluster.name"
const (
ClustersOrderByCPU = "cpu"
ClustersOrderByCPUAllocatable = "cpu_allocatable"
@@ -14,5 +12,4 @@ var ClustersValidOrderByKeys = []string{
ClustersOrderByCPUAllocatable,
ClustersOrderByMemory,
ClustersOrderByMemoryAllocatable,
ClusterNameAttrKey,
}

View File

@@ -275,57 +275,6 @@ func TestPostableClusters_Validate(t *testing.T) {
},
wantErr: true,
},
{
name: "orderBy name asc with empty groupBy is valid",
req: &PostableClusters{
Start: 1000,
End: 2000,
Limit: 100,
Offset: 0,
OrderBy: &qbtypes.OrderBy{
Key: qbtypes.OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: ClusterNameAttrKey},
},
Direction: qbtypes.OrderDirectionAsc,
},
},
wantErr: false,
},
{
name: "orderBy name desc with empty groupBy is valid",
req: &PostableClusters{
Start: 1000,
End: 2000,
Limit: 100,
Offset: 0,
OrderBy: &qbtypes.OrderBy{
Key: qbtypes.OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: ClusterNameAttrKey},
},
Direction: qbtypes.OrderDirectionDesc,
},
},
wantErr: false,
},
{
name: "orderBy name with non-empty groupBy is rejected",
req: &PostableClusters{
Start: 1000,
End: 2000,
Limit: 100,
Offset: 0,
GroupBy: []qbtypes.GroupByKey{
{TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: "k8s.cluster.name"}},
},
OrderBy: &qbtypes.OrderBy{
Key: qbtypes.OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: ClusterNameAttrKey},
},
Direction: qbtypes.OrderDirectionAsc,
},
},
wantErr: true,
},
}
for _, tt := range tests {

View File

@@ -88,9 +88,6 @@ func (req *PostableDaemonSets) Validate() error {
if req.OrderBy.Direction != qbtypes.OrderDirectionAsc && req.OrderBy.Direction != qbtypes.OrderDirectionDesc {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid order by direction: %s", req.OrderBy.Direction)
}
if req.OrderBy.Key.Name == DaemonSetNameAttrKey && len(req.GroupBy) > 0 {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "order by '%s' is only allowed when groupBy is empty", DaemonSetNameAttrKey)
}
}
return nil

View File

@@ -1,14 +1,12 @@
package inframonitoringtypes
const DaemonSetNameAttrKey = "k8s.daemonset.name"
const (
DaemonSetsOrderByCPU = "cpu"
DaemonSetsOrderByCPURequest = "cpu_request"
DaemonSetsOrderByCPULimit = "cpu_limit"
DaemonSetsOrderByMemory = "memory"
DaemonSetsOrderByMemoryRequest = "memory_request"
DaemonSetsOrderByMemoryLimit = "memory_limit"
DaemonSetsOrderByCPU = "cpu"
DaemonSetsOrderByCPURequest = "cpu_request"
DaemonSetsOrderByCPULimit = "cpu_limit"
DaemonSetsOrderByMemory = "memory"
DaemonSetsOrderByMemoryRequest = "memory_request"
DaemonSetsOrderByMemoryLimit = "memory_limit"
DaemonSetsOrderByDesiredNodes = "desired_nodes"
DaemonSetsOrderByCurrentNodes = "current_nodes"
)
@@ -22,5 +20,4 @@ var DaemonSetsValidOrderByKeys = []string{
DaemonSetsOrderByMemoryLimit,
DaemonSetsOrderByDesiredNodes,
DaemonSetsOrderByCurrentNodes,
DaemonSetNameAttrKey,
}

View File

@@ -257,63 +257,6 @@ func TestPostableDaemonSets_Validate(t *testing.T) {
},
wantErr: true,
},
{
name: "orderBy name asc with empty groupBy is valid",
req: &PostableDaemonSets{
Start: 1000,
End: 2000,
Limit: 100,
Offset: 0,
OrderBy: &qbtypes.OrderBy{
Key: qbtypes.OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: DaemonSetNameAttrKey,
},
},
Direction: qbtypes.OrderDirectionAsc,
},
},
wantErr: false,
},
{
name: "orderBy name desc with empty groupBy is valid",
req: &PostableDaemonSets{
Start: 1000,
End: 2000,
Limit: 100,
Offset: 0,
OrderBy: &qbtypes.OrderBy{
Key: qbtypes.OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: DaemonSetNameAttrKey,
},
},
Direction: qbtypes.OrderDirectionDesc,
},
},
wantErr: false,
},
{
name: "orderBy name with non-empty groupBy is rejected",
req: &PostableDaemonSets{
Start: 1000,
End: 2000,
Limit: 100,
Offset: 0,
GroupBy: []qbtypes.GroupByKey{
{TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: "k8s.namespace.name"}},
},
OrderBy: &qbtypes.OrderBy{
Key: qbtypes.OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: DaemonSetNameAttrKey,
},
},
Direction: qbtypes.OrderDirectionAsc,
},
},
wantErr: true,
},
}
for _, tt := range tests {

View File

@@ -88,9 +88,6 @@ func (req *PostableDeployments) Validate() error {
if req.OrderBy.Direction != qbtypes.OrderDirectionAsc && req.OrderBy.Direction != qbtypes.OrderDirectionDesc {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid order by direction: %s", req.OrderBy.Direction)
}
if req.OrderBy.Key.Name == DeploymentNameAttrKey && len(req.GroupBy) > 0 {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "order by '%s' is only allowed when groupBy is empty", DeploymentNameAttrKey)
}
}
return nil

View File

@@ -1,7 +1,5 @@
package inframonitoringtypes
const DeploymentNameAttrKey = "k8s.deployment.name"
const (
DeploymentsOrderByCPU = "cpu"
DeploymentsOrderByCPURequest = "cpu_request"
@@ -22,5 +20,4 @@ var DeploymentsValidOrderByKeys = []string{
DeploymentsOrderByMemoryLimit,
DeploymentsOrderByDesiredPods,
DeploymentsOrderByAvailablePods,
DeploymentNameAttrKey,
}

View File

@@ -257,57 +257,6 @@ func TestPostableDeployments_Validate(t *testing.T) {
},
wantErr: true,
},
{
name: "orderBy name asc with empty groupBy is valid",
req: &PostableDeployments{
Start: 1000,
End: 2000,
Limit: 100,
Offset: 0,
OrderBy: &qbtypes.OrderBy{
Key: qbtypes.OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: DeploymentNameAttrKey},
},
Direction: qbtypes.OrderDirectionAsc,
},
},
wantErr: false,
},
{
name: "orderBy name desc with empty groupBy is valid",
req: &PostableDeployments{
Start: 1000,
End: 2000,
Limit: 100,
Offset: 0,
OrderBy: &qbtypes.OrderBy{
Key: qbtypes.OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: DeploymentNameAttrKey},
},
Direction: qbtypes.OrderDirectionDesc,
},
},
wantErr: false,
},
{
name: "orderBy name with non-empty groupBy is rejected",
req: &PostableDeployments{
Start: 1000,
End: 2000,
Limit: 100,
Offset: 0,
GroupBy: []qbtypes.GroupByKey{
{TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: "k8s.namespace.name"}},
},
OrderBy: &qbtypes.OrderBy{
Key: qbtypes.OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: DeploymentNameAttrKey},
},
Direction: qbtypes.OrderDirectionAsc,
},
},
wantErr: true,
},
}
for _, tt := range tests {

View File

@@ -100,9 +100,6 @@ func (req *PostableHosts) Validate() error {
if req.OrderBy.Direction != qbtypes.OrderDirectionAsc && req.OrderBy.Direction != qbtypes.OrderDirectionDesc {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid order by direction: %s", req.OrderBy.Direction)
}
if req.OrderBy.Key.Name == HostNameAttrKey && len(req.GroupBy) > 0 {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "order by '%s' is only allowed when groupBy is empty", HostNameAttrKey)
}
}
return nil

View File

@@ -20,8 +20,6 @@ func (HostStatus) Enum() []any {
}
}
const HostNameAttrKey = "host.name"
const (
HostsOrderByCPU = "cpu"
HostsOrderByMemory = "memory"
@@ -36,5 +34,4 @@ var HostsValidOrderByKeys = []string{
HostsOrderByWait,
HostsOrderByDiskUsage,
HostsOrderByLoad15,
HostNameAttrKey,
}

View File

@@ -228,57 +228,6 @@ func TestHostsListRequest_Validate(t *testing.T) {
},
wantErr: true,
},
{
name: "orderBy name asc with empty groupBy is valid",
req: &PostableHosts{
Start: 1000,
End: 2000,
Limit: 100,
Offset: 0,
OrderBy: &qbtypes.OrderBy{
Key: qbtypes.OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: HostNameAttrKey},
},
Direction: qbtypes.OrderDirectionAsc,
},
},
wantErr: false,
},
{
name: "orderBy name desc with empty groupBy is valid",
req: &PostableHosts{
Start: 1000,
End: 2000,
Limit: 100,
Offset: 0,
OrderBy: &qbtypes.OrderBy{
Key: qbtypes.OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: HostNameAttrKey},
},
Direction: qbtypes.OrderDirectionDesc,
},
},
wantErr: false,
},
{
name: "orderBy name with non-empty groupBy is rejected",
req: &PostableHosts{
Start: 1000,
End: 2000,
Limit: 100,
Offset: 0,
GroupBy: []qbtypes.GroupByKey{
{TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: "os.type"}},
},
OrderBy: &qbtypes.OrderBy{
Key: qbtypes.OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: HostNameAttrKey},
},
Direction: qbtypes.OrderDirectionAsc,
},
},
wantErr: true,
},
}
for _, tt := range tests {

View File

@@ -90,9 +90,6 @@ func (req *PostableJobs) Validate() error {
if req.OrderBy.Direction != qbtypes.OrderDirectionAsc && req.OrderBy.Direction != qbtypes.OrderDirectionDesc {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid order by direction: %s", req.OrderBy.Direction)
}
if req.OrderBy.Key.Name == JobNameAttrKey && len(req.GroupBy) > 0 {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "order by '%s' is only allowed when groupBy is empty", JobNameAttrKey)
}
}
return nil

View File

@@ -1,7 +1,5 @@
package inframonitoringtypes
const JobNameAttrKey = "k8s.job.name"
const (
JobsOrderByCPU = "cpu"
JobsOrderByCPURequest = "cpu_request"
@@ -26,5 +24,4 @@ var JobsValidOrderByKeys = []string{
JobsOrderByActivePods,
JobsOrderByFailedPods,
JobsOrderBySuccessfulPods,
JobNameAttrKey,
}

View File

@@ -293,57 +293,6 @@ func TestPostableJobs_Validate(t *testing.T) {
},
wantErr: true,
},
{
name: "orderBy name asc with empty groupBy is valid",
req: &PostableJobs{
Start: 1000,
End: 2000,
Limit: 100,
Offset: 0,
OrderBy: &qbtypes.OrderBy{
Key: qbtypes.OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: JobNameAttrKey},
},
Direction: qbtypes.OrderDirectionAsc,
},
},
wantErr: false,
},
{
name: "orderBy name desc with empty groupBy is valid",
req: &PostableJobs{
Start: 1000,
End: 2000,
Limit: 100,
Offset: 0,
OrderBy: &qbtypes.OrderBy{
Key: qbtypes.OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: JobNameAttrKey},
},
Direction: qbtypes.OrderDirectionDesc,
},
},
wantErr: false,
},
{
name: "orderBy name with non-empty groupBy is rejected",
req: &PostableJobs{
Start: 1000,
End: 2000,
Limit: 100,
Offset: 0,
GroupBy: []qbtypes.GroupByKey{
{TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: "k8s.namespace.name"}},
},
OrderBy: &qbtypes.OrderBy{
Key: qbtypes.OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: JobNameAttrKey},
},
Direction: qbtypes.OrderDirectionAsc,
},
},
wantErr: true,
},
}
for _, tt := range tests {

View File

@@ -82,9 +82,6 @@ func (req *PostableNamespaces) Validate() error {
if req.OrderBy.Direction != qbtypes.OrderDirectionAsc && req.OrderBy.Direction != qbtypes.OrderDirectionDesc {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid order by direction: %s", req.OrderBy.Direction)
}
if req.OrderBy.Key.Name == NamespaceNameAttrKey && len(req.GroupBy) > 0 {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "order by '%s' is only allowed when groupBy is empty", NamespaceNameAttrKey)
}
}
return nil

View File

@@ -1,7 +1,5 @@
package inframonitoringtypes
const NamespaceNameAttrKey = "k8s.namespace.name"
const (
NamespacesOrderByCPU = "cpu"
NamespacesOrderByMemory = "memory"
@@ -10,5 +8,4 @@ const (
var NamespacesValidOrderByKeys = []string{
NamespacesOrderByCPU,
NamespacesOrderByMemory,
NamespaceNameAttrKey,
}

View File

@@ -221,57 +221,6 @@ func TestPostableNamespaces_Validate(t *testing.T) {
},
wantErr: true,
},
{
name: "orderBy name asc with empty groupBy is valid",
req: &PostableNamespaces{
Start: 1000,
End: 2000,
Limit: 100,
Offset: 0,
OrderBy: &qbtypes.OrderBy{
Key: qbtypes.OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: NamespaceNameAttrKey},
},
Direction: qbtypes.OrderDirectionAsc,
},
},
wantErr: false,
},
{
name: "orderBy name desc with empty groupBy is valid",
req: &PostableNamespaces{
Start: 1000,
End: 2000,
Limit: 100,
Offset: 0,
OrderBy: &qbtypes.OrderBy{
Key: qbtypes.OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: NamespaceNameAttrKey},
},
Direction: qbtypes.OrderDirectionDesc,
},
},
wantErr: false,
},
{
name: "orderBy name with non-empty groupBy is rejected",
req: &PostableNamespaces{
Start: 1000,
End: 2000,
Limit: 100,
Offset: 0,
GroupBy: []qbtypes.GroupByKey{
{TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: "k8s.cluster.name"}},
},
OrderBy: &qbtypes.OrderBy{
Key: qbtypes.OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: NamespaceNameAttrKey},
},
Direction: qbtypes.OrderDirectionAsc,
},
},
wantErr: true,
},
}
for _, tt := range tests {

View File

@@ -93,9 +93,6 @@ func (req *PostableNodes) Validate() error {
if req.OrderBy.Direction != qbtypes.OrderDirectionAsc && req.OrderBy.Direction != qbtypes.OrderDirectionDesc {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid order by direction: %s", req.OrderBy.Direction)
}
if req.OrderBy.Key.Name == NodeNameAttrKey && len(req.GroupBy) > 0 {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "order by '%s' is only allowed when groupBy is empty", NodeNameAttrKey)
}
}
return nil

View File

@@ -27,8 +27,6 @@ const (
NodeConditionNumNotReady = 0
)
const NodeNameAttrKey = "k8s.node.name"
const (
NodesOrderByCPU = "cpu"
NodesOrderByCPUAllocatable = "cpu_allocatable"
@@ -41,5 +39,4 @@ var NodesValidOrderByKeys = []string{
NodesOrderByCPUAllocatable,
NodesOrderByMemory,
NodesOrderByMemoryAllocatable,
NodeNameAttrKey,
}

View File

@@ -239,57 +239,6 @@ func TestPostableNodes_Validate(t *testing.T) {
},
wantErr: true,
},
{
name: "orderBy name asc with empty groupBy is valid",
req: &PostableNodes{
Start: 1000,
End: 2000,
Limit: 100,
Offset: 0,
OrderBy: &qbtypes.OrderBy{
Key: qbtypes.OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: NodeNameAttrKey},
},
Direction: qbtypes.OrderDirectionAsc,
},
},
wantErr: false,
},
{
name: "orderBy name desc with empty groupBy is valid",
req: &PostableNodes{
Start: 1000,
End: 2000,
Limit: 100,
Offset: 0,
OrderBy: &qbtypes.OrderBy{
Key: qbtypes.OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: NodeNameAttrKey},
},
Direction: qbtypes.OrderDirectionDesc,
},
},
wantErr: false,
},
{
name: "orderBy name with non-empty groupBy is rejected",
req: &PostableNodes{
Start: 1000,
End: 2000,
Limit: 100,
Offset: 0,
GroupBy: []qbtypes.GroupByKey{
{TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: "k8s.cluster.name"}},
},
OrderBy: &qbtypes.OrderBy{
Key: qbtypes.OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: NodeNameAttrKey},
},
Direction: qbtypes.OrderDirectionAsc,
},
},
wantErr: true,
},
}
for _, tt := range tests {

View File

@@ -1,55 +0,0 @@
package inframonitoringtypes
import (
"sort"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
)
// PaginateMetadataByName returns metadataMap groups sorted by name
// (lexicographic, asc or desc), paginated by offset/limit, and rebuilt into
// label maps using groupBy. When sortByMetaKey is non-empty, groups are sorted
// by metadataMap[k][sortByMetaKey]; otherwise sorted by composite key directly.
func PaginateMetadataByName(
metadataMap map[string]map[string]string,
groupBy []qbtypes.GroupByKey,
direction qbtypes.OrderDirection,
offset, limit int,
sortByMetaKey string,
) []map[string]string {
pageGroups := make([]map[string]string, 0)
if offset >= len(metadataMap) {
return pageGroups
}
type entry struct{ compositeKey, sortVal string }
entries := make([]entry, 0, len(metadataMap))
for ck, meta := range metadataMap {
sv := ck
if sortByMetaKey != "" {
sv = meta[sortByMetaKey]
}
entries = append(entries, entry{compositeKey: ck, sortVal: sv})
}
sort.Slice(entries, func(i, j int) bool {
if entries[i].sortVal != entries[j].sortVal {
if direction == qbtypes.OrderDirectionAsc {
return entries[i].sortVal < entries[j].sortVal
}
return entries[i].sortVal > entries[j].sortVal
}
return entries[i].compositeKey < entries[j].compositeKey
})
end := min(offset+limit, len(entries))
for _, e := range entries[offset:end] {
attrs := metadataMap[e.compositeKey]
labels := make(map[string]string, len(groupBy))
for _, gb := range groupBy {
labels[gb.Name] = attrs[gb.Name]
}
pageGroups = append(pageGroups, labels)
}
return pageGroups
}

View File

@@ -1,371 +0,0 @@
package inframonitoringtypes
import (
"testing"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/stretchr/testify/assert"
)
func gbKey(name string) qbtypes.GroupByKey {
return qbtypes.GroupByKey{TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: name}}
}
// fiveHostMap returns a metadataMap with 5 host entries h1..h5.
func fiveHostMap() map[string]map[string]string {
m := make(map[string]map[string]string, 5)
for _, n := range []string{"h1", "h2", "h3", "h4", "h5"} {
m[n] = map[string]string{"host.name": n}
}
return m
}
func TestPaginateMetadataByName(t *testing.T) {
hostGB := []qbtypes.GroupByKey{gbKey("host.name")}
tests := []struct {
name string
metadataMap map[string]map[string]string
groupBy []qbtypes.GroupByKey
direction qbtypes.OrderDirection
offset, limit int
sortByMetaKey string
want []map[string]string
wantNotNil bool // assert empty non-nil slice instead of comparing to want
}{
// A. Array out of bounds
{
name: "offset_equals_len",
metadataMap: fiveHostMap(),
groupBy: hostGB,
direction: qbtypes.OrderDirectionAsc,
offset: 5,
limit: 10,
sortByMetaKey: "host.name",
wantNotNil: true,
},
{
name: "offset_way_past_len",
metadataMap: fiveHostMap(),
groupBy: hostGB,
direction: qbtypes.OrderDirectionAsc,
offset: 100,
limit: 10,
sortByMetaKey: "host.name",
wantNotNil: true,
},
{
name: "offset_plus_limit_exceeds_len",
metadataMap: fiveHostMap(),
groupBy: hostGB,
direction: qbtypes.OrderDirectionAsc,
offset: 3,
limit: 10,
sortByMetaKey: "host.name",
want: []map[string]string{
{"host.name": "h4"},
{"host.name": "h5"},
},
},
{
name: "limit_exceeds_len",
metadataMap: fiveHostMap(),
groupBy: hostGB,
direction: qbtypes.OrderDirectionAsc,
offset: 0,
limit: 100,
sortByMetaKey: "host.name",
want: []map[string]string{
{"host.name": "h1"},
{"host.name": "h2"},
{"host.name": "h3"},
{"host.name": "h4"},
{"host.name": "h5"},
},
},
{
name: "limit_zero",
metadataMap: fiveHostMap(),
groupBy: hostGB,
direction: qbtypes.OrderDirectionAsc,
offset: 0,
limit: 0,
sortByMetaKey: "host.name",
wantNotNil: true, // expect empty non-nil slice
},
{
name: "empty_map",
metadataMap: map[string]map[string]string{},
groupBy: hostGB,
direction: qbtypes.OrderDirectionAsc,
offset: 0,
limit: 10,
sortByMetaKey: "host.name",
wantNotNil: true,
},
{
name: "exact_page",
metadataMap: fiveHostMap(),
groupBy: hostGB,
direction: qbtypes.OrderDirectionAsc,
offset: 0,
limit: 5,
sortByMetaKey: "host.name",
want: []map[string]string{
{"host.name": "h1"},
{"host.name": "h2"},
{"host.name": "h3"},
{"host.name": "h4"},
{"host.name": "h5"},
},
},
{
name: "mid_page",
metadataMap: fiveHostMap(),
groupBy: hostGB,
direction: qbtypes.OrderDirectionAsc,
offset: 2,
limit: 2,
sortByMetaKey: "host.name",
want: []map[string]string{
{"host.name": "h3"},
{"host.name": "h4"},
},
},
{
name: "last_single",
metadataMap: fiveHostMap(),
groupBy: hostGB,
direction: qbtypes.OrderDirectionAsc,
offset: 4,
limit: 1,
sortByMetaKey: "host.name",
want: []map[string]string{
{"host.name": "h5"},
},
},
// B. Nil / missing
{
name: "nil_map",
metadataMap: nil,
groupBy: hostGB,
direction: qbtypes.OrderDirectionAsc,
offset: 0,
limit: 10,
sortByMetaKey: "host.name",
wantNotNil: true,
},
{
name: "nil_groupBy",
metadataMap: map[string]map[string]string{
"h1": {"host.name": "h1"},
"h2": {"host.name": "h2"},
},
groupBy: nil,
direction: qbtypes.OrderDirectionAsc,
offset: 0,
limit: 10,
sortByMetaKey: "host.name",
want: []map[string]string{
{},
{},
},
},
{
name: "empty_groupBy",
metadataMap: map[string]map[string]string{
"h1": {"host.name": "h1"},
"h2": {"host.name": "h2"},
},
groupBy: []qbtypes.GroupByKey{},
direction: qbtypes.OrderDirectionAsc,
offset: 0,
limit: 10,
sortByMetaKey: "host.name",
want: []map[string]string{
{},
{},
},
},
{
name: "missing_key_in_entry",
metadataMap: map[string]map[string]string{
"h1": {"host.name": "h1", "os.type": "linux"},
"h2": {"host.name": "h2"}, // os.type absent
},
groupBy: []qbtypes.GroupByKey{gbKey("host.name"), gbKey("os.type")},
direction: qbtypes.OrderDirectionAsc,
offset: 0,
limit: 10,
sortByMetaKey: "host.name",
want: []map[string]string{
{"host.name": "h1", "os.type": "linux"},
{"host.name": "h2", "os.type": ""},
},
},
{
name: "nil_inner_map",
metadataMap: map[string]map[string]string{
"h1": nil,
"h2": {"host.name": "h2"},
},
groupBy: hostGB,
direction: qbtypes.OrderDirectionAsc,
offset: 0,
limit: 10,
sortByMetaKey: "host.name",
// sortVal for h1 = "" (nil map read), for h2 = "h2".
// Asc order: "" < "h2", so h1 first.
want: []map[string]string{
{"host.name": ""},
{"host.name": "h2"},
},
},
{
name: "sortByMetaKey_empty_sorts_by_compositeKey",
metadataMap: map[string]map[string]string{
"x\x00z": {"a": "x", "b": "z"},
"x\x00y": {"a": "x", "b": "y"},
},
groupBy: []qbtypes.GroupByKey{gbKey("a"), gbKey("b")},
direction: qbtypes.OrderDirectionAsc,
offset: 0,
limit: 10,
sortByMetaKey: "",
want: []map[string]string{
{"a": "x", "b": "y"},
{"a": "x", "b": "z"},
},
},
{
name: "sortByMetaKey_absent_in_some_entries",
metadataMap: map[string]map[string]string{
"h1": {"host.name": "h1"},
"h2": {}, // host.name absent
"h3": {"host.name": "h3"},
},
groupBy: hostGB,
direction: qbtypes.OrderDirectionAsc,
offset: 0,
limit: 10,
sortByMetaKey: "host.name",
// h2 sortVal="" sorts first; then h1, h3.
want: []map[string]string{
{"host.name": ""},
{"host.name": "h1"},
{"host.name": "h3"},
},
},
// C. Sort direction
{
name: "asc",
metadataMap: map[string]map[string]string{
"h2": {"host.name": "h2"},
"h1": {"host.name": "h1"},
"h3": {"host.name": "h3"},
},
groupBy: hostGB,
direction: qbtypes.OrderDirectionAsc,
offset: 0,
limit: 10,
sortByMetaKey: "host.name",
want: []map[string]string{
{"host.name": "h1"},
{"host.name": "h2"},
{"host.name": "h3"},
},
},
{
name: "desc",
metadataMap: map[string]map[string]string{
"h2": {"host.name": "h2"},
"h1": {"host.name": "h1"},
"h3": {"host.name": "h3"},
},
groupBy: hostGB,
direction: qbtypes.OrderDirectionDesc,
offset: 0,
limit: 10,
sortByMetaKey: "host.name",
want: []map[string]string{
{"host.name": "h3"},
{"host.name": "h2"},
{"host.name": "h1"},
},
},
{
name: "zero_value_direction_falls_into_desc",
metadataMap: map[string]map[string]string{
"h2": {"host.name": "h2"},
"h1": {"host.name": "h1"},
"h3": {"host.name": "h3"},
},
groupBy: hostGB,
direction: qbtypes.OrderDirection{},
offset: 0,
limit: 10,
sortByMetaKey: "host.name",
want: []map[string]string{
{"host.name": "h3"},
{"host.name": "h2"},
{"host.name": "h1"},
},
},
{
name: "tie_breaks_on_compositeKey_asc",
metadataMap: map[string]map[string]string{
"a\x00b": {"k": "a", "x": "tie"},
"a\x00c": {"k": "a", "x": "tie"},
},
groupBy: []qbtypes.GroupByKey{gbKey("k"), gbKey("x")},
direction: qbtypes.OrderDirectionDesc,
offset: 0,
limit: 10,
sortByMetaKey: "x",
// Both sortVals "tie"; tie-break on compositeKey asc:
// "a\x00b" < "a\x00c", so b first.
want: []map[string]string{
{"k": "a", "x": "tie"},
{"k": "a", "x": "tie"},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := PaginateMetadataByName(
tt.metadataMap,
tt.groupBy,
tt.direction,
tt.offset,
tt.limit,
tt.sortByMetaKey,
)
if tt.wantNotNil {
assert.NotNil(t, got)
assert.Len(t, got, 0)
return
}
assert.Equal(t, tt.want, got)
})
}
}
func TestPaginateMetadataByName_Deterministic(t *testing.T) {
m := make(map[string]map[string]string, 10)
// 10 entries, several with tied host.name values to force compositeKey tie-break.
for i, n := range []string{"a", "a", "b", "b", "c", "d", "e", "e", "f", "g"} {
ck := n + "\x00" + string(rune('0'+i))
m[ck] = map[string]string{"host.name": n, "id": string(rune('0' + i))}
}
gb := []qbtypes.GroupByKey{gbKey("host.name"), gbKey("id")}
first := PaginateMetadataByName(m, gb, qbtypes.OrderDirectionAsc, 0, 10, "host.name")
for i := range 50 {
got := PaginateMetadataByName(m, gb, qbtypes.OrderDirectionAsc, 0, 10, "host.name")
assert.Equal(t, first, got, "iteration %d differed — map-iteration nondeterminism leaked through sort", i)
}
}

View File

@@ -98,9 +98,6 @@ func (req *PostablePods) Validate() error {
if req.OrderBy.Direction != qbtypes.OrderDirectionAsc && req.OrderBy.Direction != qbtypes.OrderDirectionDesc {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid order by direction: %s", req.OrderBy.Direction)
}
if req.OrderBy.Key.Name == PodNameAttrKey && len(req.GroupBy) > 0 {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "order by '%s' is only allowed when groupBy is empty", PodNameAttrKey)
}
}
return nil

View File

@@ -36,8 +36,6 @@ const (
PodPhaseNumUnknown = 5
)
const PodNameAttrKey = "k8s.pod.name"
const (
PodsOrderByCPU = "cpu"
PodsOrderByCPURequest = "cpu_request"
@@ -54,5 +52,4 @@ var PodsValidOrderByKeys = []string{
PodsOrderByMemory,
PodsOrderByMemoryRequest,
PodsOrderByMemoryLimit,
PodNameAttrKey,
}

View File

@@ -203,57 +203,6 @@ func TestPostablePods_Validate(t *testing.T) {
},
wantErr: true,
},
{
name: "orderBy name asc with empty groupBy is valid",
req: &PostablePods{
Start: 1000,
End: 2000,
Limit: 100,
Offset: 0,
OrderBy: &qbtypes.OrderBy{
Key: qbtypes.OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: PodNameAttrKey},
},
Direction: qbtypes.OrderDirectionAsc,
},
},
wantErr: false,
},
{
name: "orderBy name desc with empty groupBy is valid",
req: &PostablePods{
Start: 1000,
End: 2000,
Limit: 100,
Offset: 0,
OrderBy: &qbtypes.OrderBy{
Key: qbtypes.OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: PodNameAttrKey},
},
Direction: qbtypes.OrderDirectionDesc,
},
},
wantErr: false,
},
{
name: "orderBy name with non-empty groupBy is rejected",
req: &PostablePods{
Start: 1000,
End: 2000,
Limit: 100,
Offset: 0,
GroupBy: []qbtypes.GroupByKey{
{TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: "k8s.namespace.name"}},
},
OrderBy: &qbtypes.OrderBy{
Key: qbtypes.OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: PodNameAttrKey},
},
Direction: qbtypes.OrderDirectionAsc,
},
},
wantErr: true,
},
}
for _, tt := range tests {

View File

@@ -88,9 +88,6 @@ func (req *PostableStatefulSets) Validate() error {
if req.OrderBy.Direction != qbtypes.OrderDirectionAsc && req.OrderBy.Direction != qbtypes.OrderDirectionDesc {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid order by direction: %s", req.OrderBy.Direction)
}
if req.OrderBy.Key.Name == StatefulSetNameAttrKey && len(req.GroupBy) > 0 {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "order by '%s' is only allowed when groupBy is empty", StatefulSetNameAttrKey)
}
}
return nil

View File

@@ -1,7 +1,5 @@
package inframonitoringtypes
const StatefulSetNameAttrKey = "k8s.statefulset.name"
const (
StatefulSetsOrderByCPU = "cpu"
StatefulSetsOrderByCPURequest = "cpu_request"
@@ -22,5 +20,4 @@ var StatefulSetsValidOrderByKeys = []string{
StatefulSetsOrderByMemoryLimit,
StatefulSetsOrderByDesiredPods,
StatefulSetsOrderByCurrentPods,
StatefulSetNameAttrKey,
}

View File

@@ -257,57 +257,6 @@ func TestPostableStatefulSets_Validate(t *testing.T) {
},
wantErr: true,
},
{
name: "orderBy name asc with empty groupBy is valid",
req: &PostableStatefulSets{
Start: 1000,
End: 2000,
Limit: 100,
Offset: 0,
OrderBy: &qbtypes.OrderBy{
Key: qbtypes.OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: StatefulSetNameAttrKey},
},
Direction: qbtypes.OrderDirectionAsc,
},
},
wantErr: false,
},
{
name: "orderBy name desc with empty groupBy is valid",
req: &PostableStatefulSets{
Start: 1000,
End: 2000,
Limit: 100,
Offset: 0,
OrderBy: &qbtypes.OrderBy{
Key: qbtypes.OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: StatefulSetNameAttrKey},
},
Direction: qbtypes.OrderDirectionDesc,
},
},
wantErr: false,
},
{
name: "orderBy name with non-empty groupBy is rejected",
req: &PostableStatefulSets{
Start: 1000,
End: 2000,
Limit: 100,
Offset: 0,
GroupBy: []qbtypes.GroupByKey{
{TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: "k8s.namespace.name"}},
},
OrderBy: &qbtypes.OrderBy{
Key: qbtypes.OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: StatefulSetNameAttrKey},
},
Direction: qbtypes.OrderDirectionAsc,
},
},
wantErr: true,
},
}
for _, tt := range tests {

View File

@@ -85,9 +85,6 @@ func (req *PostableVolumes) Validate() error {
if req.OrderBy.Direction != qbtypes.OrderDirectionAsc && req.OrderBy.Direction != qbtypes.OrderDirectionDesc {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid order by direction: %s", req.OrderBy.Direction)
}
if req.OrderBy.Key.Name == PersistentVolumeClaimNameAttrKey && len(req.GroupBy) > 0 {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "order by '%s' is only allowed when groupBy is empty", PersistentVolumeClaimNameAttrKey)
}
}
return nil

View File

@@ -1,7 +1,5 @@
package inframonitoringtypes
const PersistentVolumeClaimNameAttrKey = "k8s.persistentvolumeclaim.name"
const (
VolumesOrderByAvailable = "available"
VolumesOrderByCapacity = "capacity"
@@ -18,5 +16,4 @@ var VolumesValidOrderByKeys = []string{
VolumesOrderByInodes,
VolumesOrderByInodesFree,
VolumesOrderByInodesUsed,
PersistentVolumeClaimNameAttrKey,
}

View File

@@ -221,57 +221,6 @@ func TestPostableVolumes_Validate(t *testing.T) {
},
wantErr: true,
},
{
name: "orderBy name asc with empty groupBy is valid",
req: &PostableVolumes{
Start: 1000,
End: 2000,
Limit: 100,
Offset: 0,
OrderBy: &qbtypes.OrderBy{
Key: qbtypes.OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: PersistentVolumeClaimNameAttrKey},
},
Direction: qbtypes.OrderDirectionAsc,
},
},
wantErr: false,
},
{
name: "orderBy name desc with empty groupBy is valid",
req: &PostableVolumes{
Start: 1000,
End: 2000,
Limit: 100,
Offset: 0,
OrderBy: &qbtypes.OrderBy{
Key: qbtypes.OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: PersistentVolumeClaimNameAttrKey},
},
Direction: qbtypes.OrderDirectionDesc,
},
},
wantErr: false,
},
{
name: "orderBy name with non-empty groupBy is rejected",
req: &PostableVolumes{
Start: 1000,
End: 2000,
Limit: 100,
Offset: 0,
GroupBy: []qbtypes.GroupByKey{
{TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: "k8s.namespace.name"}},
},
OrderBy: &qbtypes.OrderBy{
Key: qbtypes.OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: PersistentVolumeClaimNameAttrKey},
},
Direction: qbtypes.OrderDirectionAsc,
},
},
wantErr: true,
},
}
for _, tt := range tests {