diff --git a/common/constants.ts b/common/constants.ts
index 2186516..e2bb8de 100644
--- a/common/constants.ts
+++ b/common/constants.ts
@@ -4,6 +4,9 @@
*/
export const TIMESTAMP = 'Timestamp';
+export const TYPE = 'Type';
+export const QUERY_HASHCODE = 'Query Hashcode';
+export const QUERY_COUNT = 'Query Count';
export const LATENCY = 'Latency';
export const CPU_TIME = 'CPU Time';
export const MEMORY_USAGE = 'Memory Usage';
@@ -11,3 +14,4 @@ export const INDICES = 'Indices';
export const SEARCH_TYPE = 'Search type';
export const NODE_ID = 'Coordinator node ID';
export const TOTAL_SHARDS = 'Total shards';
+export const GROUP_BY = 'Group by';
diff --git a/public/components/__snapshots__/app.test.tsx.snap b/public/components/__snapshots__/app.test.tsx.snap
index 3571697..8cd7612 100644
--- a/public/components/__snapshots__/app.test.tsx.snap
+++ b/public/components/__snapshots__/app.test.tsx.snap
@@ -99,6 +99,49 @@ exports[` spec renders the component 1`] = `
spec renders the component 1`] = `
aria-live="polite"
aria-sort="none"
class="euiTableHeaderCell"
- data-test-subj="tableHeaderCell_Timestamp_0"
+ data-test-subj="tableHeaderCell_Query Hashcode_0"
+ role="columnheader"
+ scope="col"
+ >
+
+
+
+
+
spec', () => {
it('renders the component', () => {
diff --git a/public/pages/Configuration/Configuration.tsx b/public/pages/Configuration/Configuration.tsx
index 735fb6a..1d590a7 100644
--- a/public/pages/Configuration/Configuration.tsx
+++ b/public/pages/Configuration/Configuration.tsx
@@ -3,7 +3,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import React, { useMemo, useCallback, useState, useEffect } from 'react';
+import React, { useCallback, useState, useEffect } from 'react';
import {
EuiBottomBar,
EuiButton,
@@ -24,18 +24,20 @@ import {
} from '@elastic/eui';
import { useHistory, useLocation } from 'react-router-dom';
import { CoreStart } from 'opensearch-dashboards/public';
-import { QUERY_INSIGHTS, MetricSettings } from '../TopNQueries/TopNQueries';
+import {QUERY_INSIGHTS, MetricSettings, GroupBySettings} from '../TopNQueries/TopNQueries';
const Configuration = ({
latencySettings,
cpuSettings,
memorySettings,
+ groupBySettings,
configInfo,
core,
}: {
latencySettings: MetricSettings;
cpuSettings: MetricSettings;
memorySettings: MetricSettings;
+ groupBySettings: GroupBySettings;
configInfo: any;
core: CoreStart;
}) => {
@@ -57,6 +59,11 @@ const Configuration = ({
{ value: '30', text: '30' },
];
+ const groupByOptions = [
+ { value: 'none', text: 'None' },
+ { value: 'similarity', text: 'Similarity' }
+ ];
+
const history = useHistory();
const location = useLocation();
@@ -65,15 +72,25 @@ const Configuration = ({
const [topNSize, setTopNSize] = useState(latencySettings.currTopN);
const [windowSize, setWindowSize] = useState(latencySettings.currWindowSize);
const [time, setTime] = useState(latencySettings.currTimeUnit);
+ const [groupBy, setGroupBy] = useState(groupBySettings.groupBy);
- const metricSettingsMap = useMemo(
- () => ({
+ const [metricSettingsMap, setMetricSettingsMap] = useState({
+ latency: latencySettings,
+ cpu: cpuSettings,
+ memory: memorySettings,
+ groupBy: groupBySettings
+ });
+
+ useEffect(() => {
+ setMetricSettingsMap({
latency: latencySettings,
cpu: cpuSettings,
memory: memorySettings,
- }),
- [latencySettings, cpuSettings, memorySettings]
- );
+ groupBy: groupBySettings
+ });
+
+ setGroupBy(groupBySettings.groupBy);
+ }, [latencySettings, cpuSettings, memorySettings, groupBySettings]);
const newOrReset = useCallback(() => {
const currMetric = metricSettingsMap[metric];
@@ -85,7 +102,7 @@ const Configuration = ({
useEffect(() => {
newOrReset();
- }, [newOrReset]);
+ }, [newOrReset, metricSettingsMap]);
useEffect(() => {
core.chrome.setBreadcrumbs([
@@ -122,6 +139,10 @@ const Configuration = ({
setTime(e.target.value);
};
+ const onGroupByChange = (e: any) => {
+ setGroupBy(e.target.value);
+ };
+
const MinutesBox = () => (
{
const nVal = parseInt(topNSize, 10);
@@ -317,6 +339,66 @@ const Configuration = ({
+
+
+
+
+
+
+
+ Top n queries grouping configuration settings
+
+
+
+
+
+
+
+ Group By
+
+
+ Specify the group by type.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Statuses for group by
+
+
+
+
+
+
+ Group By
+
+
+
+ {groupBySettings.groupBy == 'similarity' ? enabledSymb : disabledSymb}
+
+
+
+
+
+
{isChanged && isValid ? (
@@ -332,7 +414,7 @@ const Configuration = ({
size="s"
iconType="check"
onClick={() => {
- configInfo(false, isEnabled, metric, topNSize, windowSize, time);
+ configInfo(false, isEnabled, metric, topNSize, windowSize, time, groupBy);
return history.push(QUERY_INSIGHTS);
}}
>
diff --git a/public/pages/QueryDetails/Components/QuerySummary.tsx b/public/pages/QueryDetails/Components/QuerySummary.tsx
index cd10517..1463d75 100644
--- a/public/pages/QueryDetails/Components/QuerySummary.tsx
+++ b/public/pages/QueryDetails/Components/QuerySummary.tsx
@@ -43,16 +43,30 @@ const QuerySummary = ({ query }: { query: SearchQueryRecord }) => {
-
+
+
-
diff --git a/public/pages/QueryGroupDetails/Components/QueryGroupAggregateSummary.test.tsx b/public/pages/QueryGroupDetails/Components/QueryGroupAggregateSummary.test.tsx
new file mode 100644
index 0000000..05fb406
--- /dev/null
+++ b/public/pages/QueryGroupDetails/Components/QueryGroupAggregateSummary.test.tsx
@@ -0,0 +1,97 @@
+import { render, screen } from '@testing-library/react';
+import QueryGroupAggregateSummary from './QueryGroupAggregateSummary';
+import React from "react";
+import {mockQueries} from "../../../../test/mocks/mockQueries";
+import '@testing-library/jest-dom/extend-expect';
+import {MemoryRouter, Route} from "react-router-dom";
+
+describe('QueryGroupAggregateSummary', () => {
+ const expectedHash = '8c1e50c035663459d567fa11d8eb494d';
+
+ it('renders aggregate summary correctly', () => {
+ render(
+
+
+
+
+
+ );
+
+ expect(screen.getByText('Aggregate summary for 8 queries')).toBeInTheDocument();
+ expect(screen.getByText('Query Hashcode')).toBeInTheDocument();
+ expect(screen.getByText('Latency')).toBeInTheDocument();
+ expect(screen.getByText('CPU Time')).toBeInTheDocument();
+ expect(screen.getByText('Memory Usage')).toBeInTheDocument();
+ expect(screen.getByText('Group by')).toBeInTheDocument();
+ });
+
+ it('calculates and displays correct latency', () => {
+ render(
+
+
+
+
+
+ );
+
+ const latency = '2.50 ms';
+ expect(screen.getByText(latency)).toBeInTheDocument();
+ });
+
+ it('calculates and displays correct CPU time', () => {
+ render(
+
+
+
+
+
+ );
+
+ const cpuTime = '1.42 ms';
+ expect(screen.getByText(cpuTime)).toBeInTheDocument();
+ });
+
+ it('calculates and displays correct memory usage', () => {
+ render(
+
+
+
+
+
+ );
+
+ const memoryUsage = '132224 B';
+ expect(screen.getByText(memoryUsage)).toBeInTheDocument();
+ });
+
+ it('displays correct query hashcode', () => {
+ render(
+
+
+
+
+
+ );
+
+ const hashcode = mockQueries[0].query_hashcode;
+ expect(screen.getByText(hashcode)).toBeInTheDocument();
+ });
+
+ it('displays correct group_by value when SIMILARITY', () => {
+ const queryWithSimilarity = {
+ ...mockQueries[0],
+ group_by: 'SIMILARITY',
+ };
+
+ render(
+
+
+
+
+
+ );
+
+ expect(screen.getByText('Group by')).toBeInTheDocument();
+ expect(screen.getByText('SIMILARITY')).toBeInTheDocument(); // Verifies the "group_by" value is rendered
+ });
+});
diff --git a/public/pages/QueryGroupDetails/Components/QueryGroupAggregateSummary.tsx b/public/pages/QueryGroupDetails/Components/QueryGroupAggregateSummary.tsx
new file mode 100644
index 0000000..1f65798
--- /dev/null
+++ b/public/pages/QueryGroupDetails/Components/QueryGroupAggregateSummary.tsx
@@ -0,0 +1,75 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from 'react';
+import { EuiFlexGrid, EuiFlexItem, EuiHorizontalRule, EuiPanel, EuiText } from '@elastic/eui';
+import {
+ CPU_TIME, GROUP_BY,
+ LATENCY,
+ MEMORY_USAGE, QUERY_HASHCODE
+} from '../../../../common/constants';
+
+// Panel component for displaying query group detail values
+const PanelItem = ({ label, value }: { label: string; value: string | number }) => (
+
+
+ {label}
+
+ {value}
+
+);
+
+const QueryGroupAggregateSummary = ({ query }: { query: any }) => {
+
+ const { measurements, query_hashcode, group_by } = query;
+ const queryCount = measurements.latency?.count || measurements.cpu?.count || measurements.memory?.count || 1;
+ return (
+
+
+
+ Aggregate summary for {queryCount} {queryCount === 1 ? 'query' : 'queries'}
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default QueryGroupAggregateSummary;
diff --git a/public/pages/QueryGroupDetails/Components/QueryGroupSampleQuerySummary.test.tsx b/public/pages/QueryGroupDetails/Components/QueryGroupSampleQuerySummary.test.tsx
new file mode 100644
index 0000000..620cafd
--- /dev/null
+++ b/public/pages/QueryGroupDetails/Components/QueryGroupSampleQuerySummary.test.tsx
@@ -0,0 +1,89 @@
+import { render, screen } from '@testing-library/react';
+import React from "react";
+import {mockQueries} from "../../../../test/mocks/mockQueries";
+import {MemoryRouter, Route} from "react-router-dom";
+import QueryGroupSampleQuerySummary from "./QueryGroupSampleQuerySummary";
+import '@testing-library/jest-dom/extend-expect';
+
+describe('QueryGroupSampleQuerySummary', () => {
+ const expectedHash = '8c1e50c035663459d567fa11d8eb494d';
+
+ it('renders sample query summary correctly', () => {
+ render(
+
+
+
+
+
+ );
+
+ expect(screen.getByText('Sample query summary')).toBeInTheDocument();
+ expect(screen.getByText('Timestamp')).toBeInTheDocument();
+ expect(screen.getByText('Indices')).toBeInTheDocument();
+ expect(screen.getByText('Search type')).toBeInTheDocument();
+ expect(screen.getByText('Coordinator node ID')).toBeInTheDocument();
+ expect(screen.getByText('Total shards')).toBeInTheDocument();
+ });
+
+ it('displays correct indices value', () => {
+ render(
+
+
+
+
+
+ );
+
+ const indices = mockQueries[0].indices.join(', ');
+ expect(screen.getByText(indices)).toBeInTheDocument();
+ });
+
+ it('displays correct search type', () => {
+ render(
+
+
+
+
+
+ );
+
+ expect(screen.getByText('query then fetch')).toBeInTheDocument();
+ });
+
+ it('displays correct node ID', () => {
+ render(
+
+
+
+
+
+ );
+
+ expect(screen.getByText(mockQueries[0].node_id)).toBeInTheDocument();
+ });
+
+ it('displays correct total shards', () => {
+ render(
+
+
+
+
+
+ );
+
+ expect(screen.getByText(mockQueries[0].total_shards)).toBeInTheDocument();
+ });
+
+ it('formats timestamp correctly', () => {
+ render(
+
+
+
+
+
+ );
+
+ const formattedTimestamp = 'Nov 15, 2024 @ 12:36:12 PM';
+ expect(screen.getByText(formattedTimestamp)).toBeInTheDocument();
+ });
+});
diff --git a/public/pages/QueryGroupDetails/Components/QueryGroupSampleQuerySummary.tsx b/public/pages/QueryGroupDetails/Components/QueryGroupSampleQuerySummary.tsx
new file mode 100644
index 0000000..ae52f60
--- /dev/null
+++ b/public/pages/QueryGroupDetails/Components/QueryGroupSampleQuerySummary.tsx
@@ -0,0 +1,51 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from 'react';
+import { EuiFlexGrid, EuiFlexItem, EuiHorizontalRule, EuiPanel, EuiText } from '@elastic/eui';
+import {
+ INDICES,
+ NODE_ID,
+ SEARCH_TYPE,
+ TIMESTAMP,
+ TOTAL_SHARDS,
+} from '../../../../common/constants';
+
+const PanelItem = ({ label, value }: { label: string; value: string | number }) => (
+
+
+ {label}
+
+ {value}
+
+);
+
+const QueryGroupSampleQuerySummary = ({ query }: { query: any }) => {
+ const convertTime = (unixTime: number) => {
+ const date = new Date(unixTime);
+ const loc = date.toDateString().split(' ');
+ return `${loc[1]} ${loc[2]}, ${loc[3]} @ ${date.toLocaleTimeString('en-US')}`;
+ };
+
+
+ const { timestamp, indices, search_type, node_id, total_shards } = query;
+ return (
+
+
+ Sample query summary
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default QueryGroupSampleQuerySummary;
diff --git a/public/pages/QueryGroupDetails/QueryGroupDetails.test.tsx b/public/pages/QueryGroupDetails/QueryGroupDetails.test.tsx
new file mode 100644
index 0000000..145daad
--- /dev/null
+++ b/public/pages/QueryGroupDetails/QueryGroupDetails.test.tsx
@@ -0,0 +1,107 @@
+import { render, screen, waitFor } from '@testing-library/react';
+import {MemoryRouter, Route} from 'react-router-dom';
+import QueryGroupDetails from './QueryGroupDetails';
+import { CoreStart } from 'opensearch-dashboards/public';
+import React from 'react';
+import { mockQueries } from '../../../test/mocks/mockQueries';
+import '@testing-library/jest-dom/extend-expect';
+import hash from 'object-hash';
+
+jest.mock('object-hash', () => jest.fn(() => '8c1e50c035663459d567fa11d8eb494d'));
+
+jest.mock('plotly.js-dist', () => ({
+ newPlot: jest.fn(),
+ react: jest.fn(),
+ relayout: jest.fn(),
+}));
+
+jest.mock('react-ace', () => ({
+ __esModule: true,
+ default: () => Mocked Ace Editor ,
+}));
+
+describe('QueryGroupDetails', () => {
+ const coreMock = {
+ chrome: {
+ setBreadcrumbs: jest.fn(),
+ },
+ } as unknown as CoreStart;
+
+ const expectedHash = '8c1e50c035663459d567fa11d8eb494d';
+
+ it('renders the QueryGroupDetails component', async () => {
+ render(
+
+
+
+
+
+ );
+
+ expect(screen.getByText('Query group details')).toBeInTheDocument();
+ expect(screen.getByText('Sample query details')).toBeInTheDocument();
+
+ await waitFor(() => {
+ expect(coreMock.chrome.setBreadcrumbs).toHaveBeenCalledWith([
+ { text: 'Query insights', href: '/queryInsights' , onClick: expect.any(Function)},
+ { text: 'Query group details: Nov 15, 2024 @ 12:36:12 PM' },
+ ]);
+ });
+ });
+
+ it('renders latency bar chart', async () => {
+ render(
+
+
+
+
+
+ );
+ const latencyElements = await screen.findAllByText(/Latency/i);
+
+ expect(latencyElements.length).toBe(2);
+ });
+
+ it('displays query details', () => {
+ render(
+
+
+
+
+
+ );
+
+ expect(screen.getByText('Open in search comparision')).toBeInTheDocument();
+ });
+
+ it('should find the query based on hash', () => {
+ const expectedQuery = mockQueries.find((q: any) => hash(q) === expectedHash);
+
+ expect(expectedQuery).not.toBeUndefined();
+ if (expectedQuery) {
+ expect(expectedQuery.query_hashcode).toBe(expectedHash);
+ } else {
+ throw new Error(`Query with hash ${expectedHash} was not found in mockQueries`);
+ }
+ });
+
+ it('renders correct breadcrumb based on query hash', async () => {
+ render(
+
+
+
+
+
+ );
+
+ await waitFor(() => {
+ expect(coreMock.chrome.setBreadcrumbs).toHaveBeenCalledWith(expect.arrayContaining([
+ { text: 'Query insights', href: '/queryInsights' , onClick: expect.any(Function)},
+ { text: 'Query group details: Nov 15, 2024 @ 12:36:12 PM' },
+ expect.objectContaining({
+ text: expect.stringMatching(/^Query group details: .+/)
+ })
+ ]));
+ });
+ });
+});
diff --git a/public/pages/QueryGroupDetails/QueryGroupDetails.tsx b/public/pages/QueryGroupDetails/QueryGroupDetails.tsx
new file mode 100644
index 0000000..58c4125
--- /dev/null
+++ b/public/pages/QueryGroupDetails/QueryGroupDetails.tsx
@@ -0,0 +1,180 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { CoreStart } from 'opensearch-dashboards/public';
+// @ts-ignore
+import Plotly from 'plotly.js-dist';
+import hash from 'object-hash';
+import { useParams, useHistory, useLocation } from 'react-router-dom';
+import React, {useEffect} from "react";
+import {QUERY_INSIGHTS} from "../TopNQueries/TopNQueries";
+import {
+ EuiButton,
+ EuiCodeBlock,
+ EuiFlexGrid,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiHorizontalRule,
+ EuiPanel,
+ EuiSpacer,
+ EuiText,
+ EuiTitle,
+ EuiIconTip
+} from '@elastic/eui';
+import QueryGroupAggregateSummary from "./Components/QueryGroupAggregateSummary";
+import QueryGroupSampleQuerySummary from "./Components/QueryGroupSampleQuerySummary";
+
+const QueryGroupDetails = ({ queries, core }: { queries: any; core: CoreStart }) => {
+ const { hashedQuery } = useParams<{ hashedQuery: string }>();
+ const query = queries.find((q: any) => hash(q) === hashedQuery);
+
+ const convertTime = (unixTime: number) => {
+ const date = new Date(unixTime);
+ const loc = date.toDateString().split(' ');
+ return loc[1] + ' ' + loc[2] + ', ' + loc[3] + ' @ ' + date.toLocaleTimeString('en-US');
+ };
+
+ const history = useHistory();
+ const location = useLocation();
+
+ useEffect(() => {
+ core.chrome.setBreadcrumbs([
+ {
+ text: 'Query insights',
+ href: QUERY_INSIGHTS,
+ onClick: (e) => {
+ e.preventDefault();
+ history.push(QUERY_INSIGHTS);
+ },
+ },
+ { text: `Query group details: ${convertTime(query.timestamp)}` },
+ ]);
+ }, [core.chrome, history, location, query.timestamp]);
+
+ useEffect(() => {
+ let x: number[] = Object.values(query.phase_latency_map);
+ if (x.length < 3) {
+ x = [0, 0, 0];
+ }
+ const data = [
+ {
+ x: x.reverse(),
+ y: ['Fetch ', 'Query ', 'Expand '],
+ type: 'bar',
+ orientation: 'h',
+ width: 0.5,
+ marker: { color: ['#F990C0', '#1BA9F5', '#7DE2D1'] },
+ base: [x[2] + x[1], x[2], 0],
+ text: x.map((value) => `${value}ms`),
+ textposition: 'outside',
+ cliponaxis: false,
+ },
+ ];
+ const layout = {
+ autosize: true,
+ margin: { l: 80, r: 80, t: 25, b: 15, pad: 0 },
+ autorange: true,
+ height: 120,
+ xaxis: {
+ side: 'top',
+ zeroline: false,
+ ticksuffix: 'ms',
+ autorangeoptions: { clipmin: 0 },
+ tickfont: { color: '#535966' },
+ linecolor: '#D4DAE5',
+ gridcolor: '#D4DAE5',
+ },
+ yaxis: { linecolor: '#D4DAE5' },
+ };
+ const config = { responsive: true };
+ Plotly.newPlot('latency', data, layout, config);
+ }, [query]);
+
+ const queryString = JSON.stringify(JSON.parse(JSON.stringify(query.source)), null, 2);
+ const queryDisplay = `{\n "query": ${queryString ? queryString.replace(/\n/g, '\n ') : ''}\n}`;
+
+ return (
+
+
+
+ Query group details
+
+
+
+
+
+
+
+
+
+
+
+ Sample query details
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Query
+
+
+
+
+ Open in search comparision
+
+
+
+
+
+
+ {queryDisplay}
+
+
+
+
+
+
+ Latency
+
+
+
+
+
+
+
+
+ );
+
+};
+export default QueryGroupDetails;
diff --git a/public/pages/QueryInsights/QueryInsights.tsx b/public/pages/QueryInsights/QueryInsights.tsx
index e5b7e93..40fe2f3 100644
--- a/public/pages/QueryInsights/QueryInsights.tsx
+++ b/public/pages/QueryInsights/QueryInsights.tsx
@@ -15,10 +15,11 @@ import {
INDICES,
LATENCY,
MEMORY_USAGE,
- NODE_ID,
+ NODE_ID, QUERY_COUNT, QUERY_HASHCODE,
SEARCH_TYPE,
TIMESTAMP,
TOTAL_SHARDS,
+ TYPE,
} from '../../../common/constants';
const TIMESTAMP_FIELD = 'timestamp';
@@ -28,6 +29,7 @@ const SEARCH_TYPE_FIELD = 'search_type';
const NODE_ID_FIELD = 'node_id';
const TOTAL_SHARDS_FIELD = 'total_shards';
const METRIC_DEFAULT_MSG = 'Not enabled';
+const GROUP_BY_FIELD = 'group_by'
const QueryInsights = ({
queries,
@@ -70,16 +72,63 @@ const QueryInsights = ({
};
const cols: Array> = [
+ {
+ name: QUERY_HASHCODE,
+ render: (query: any) => {
+ return (
+
+ {
+ const route = query.group_by === 'SIMILARITY' ? `/query-group-details/${hash(query)}` : `/query-details/${hash(query)}`;
+ history.push(route);
+ }}>
+ {query.query_hashcode || '-'}
+
+
+ );
+ },
+ sortable: (query) => query.query_hashcode || '-',
+ truncateText: true,
+ },
+ {
+ name: TYPE,
+ render: (query: any) => {
+ return (
+
+ {
+ const route = query.group_by === 'SIMILARITY' ? `/query-group-details/${hash(query)}` : `/query-details/${hash(query)}`;
+ history.push(route);
+ }}>
+ {query.group_by === 'SIMILARITY' ? 'group' : 'query'}
+
+
+ );
+ },
+ sortable: (query) => query.group_by || 'query',
+ truncateText: true,
+ },
+ {
+ field: MEASUREMENTS_FIELD,
+ name: QUERY_COUNT,
+ render: (measurements: any) => `${measurements?.latency?.count || measurements?.cpu?.count || measurements?.memory?.count || 1}`,
+ sortable: (measurements: any) => {
+ return Number(measurements?.latency?.count || measurements?.cpu?.count || measurements?.memory?.count || 1);
+ },
+ truncateText: true,
+ },
{
// Make into flyout instead?
name: TIMESTAMP,
render: (query: any) => {
+ const isQuery = query.group_by === 'NONE';
+ const linkContent = isQuery ? convertTime(query.timestamp): '-';
+ const onClickHandler = () => history.push(`/query-details/${hash(query)}`);
+
return (
- history.push(`/query-details/${hash(query)}`)}>
- {convertTime(query.timestamp)}
-
-
+
+ {linkContent}
+
+
);
},
sortable: (query) => query.timestamp,
@@ -90,7 +139,11 @@ const QueryInsights = ({
name: LATENCY,
render: (measurements: any) => {
const latencyValue = measurements?.latency?.number;
- return latencyValue !== undefined ? `${latencyValue} ms` : METRIC_DEFAULT_MSG;
+ const latencyCount = measurements?.latency?.count;
+ const result = latencyValue !== undefined && latencyCount !== undefined
+ ? (latencyValue / latencyCount).toFixed(2)
+ : METRIC_DEFAULT_MSG;
+ return `${result} ms`;
},
sortable: true,
truncateText: true,
@@ -98,9 +151,13 @@ const QueryInsights = ({
{
field: MEASUREMENTS_FIELD,
name: CPU_TIME,
- render: (measurements: { cpu?: { number?: number } }) => {
+ render: (measurements: any) => {
const cpuValue = measurements?.cpu?.number;
- return cpuValue !== undefined ? `${cpuValue / 1000000} ms` : METRIC_DEFAULT_MSG;
+ const cpuCount = measurements?.cpu?.count;
+ const result = cpuValue !== undefined && cpuCount !== undefined
+ ? (cpuValue / cpuCount / 1000000).toFixed(2)
+ : METRIC_DEFAULT_MSG;
+ return `${result} ms`;
},
sortable: true,
truncateText: true,
@@ -108,9 +165,13 @@ const QueryInsights = ({
{
field: MEASUREMENTS_FIELD,
name: MEMORY_USAGE,
- render: (measurements: { memory?: { number?: number } }) => {
+ render: (measurements: any) => {
const memoryValue = measurements?.memory?.number;
- return memoryValue !== undefined ? `${memoryValue} B` : METRIC_DEFAULT_MSG;
+ const memoryCount = measurements?.memory?.count;
+ const result = memoryValue !== undefined && memoryCount !== undefined
+ ? (memoryValue / memoryCount).toFixed(2)
+ : METRIC_DEFAULT_MSG;
+ return `${result} B`;
},
sortable: true,
truncateText: true,
@@ -118,26 +179,40 @@ const QueryInsights = ({
{
field: INDICES_FIELD,
name: INDICES,
- render: (indices: string[]) => Array.from(new Set(indices.flat())).join(', '),
+ render: (indices: string[], query: any) => {
+ const isSimilarity = query.group_by === 'SIMILARITY';
+ return isSimilarity ? '-' : Array.from(new Set(indices.flat())).join(', ');
+ },
sortable: true,
truncateText: true,
},
{
field: SEARCH_TYPE_FIELD,
name: SEARCH_TYPE,
- render: (searchType: string) => searchType.replaceAll('_', ' '),
+ render: (searchType: string, query: any) => {
+ const isSimilarity = query.group_by === 'SIMILARITY';
+ return isSimilarity ? '-' : searchType.replaceAll('_', ' ');
+ },
sortable: true,
truncateText: true,
},
{
field: NODE_ID_FIELD,
name: NODE_ID,
+ render: (nodeId: string, query: any) => {
+ const isSimilarity = query.group_by === 'SIMILARITY';
+ return isSimilarity ? '-' : nodeId;
+ },
sortable: true,
truncateText: true,
},
{
field: TOTAL_SHARDS_FIELD,
name: TOTAL_SHARDS,
+ render: (totalShards: number, query: any) => {
+ const isSimilarity = query.group_by === 'SIMILARITY';
+ return isSimilarity ? '-' : totalShards;
+ },
sortable: true,
truncateText: true,
},
@@ -171,6 +246,25 @@ const QueryInsights = ({
schema: false,
},
filters: [
+ {
+ type: 'field_value_selection',
+ field: GROUP_BY_FIELD,
+ name: TYPE,
+ multiSelect: true,
+ options: [
+ {
+ value: 'NONE',
+ name: 'query',
+ view: 'query'
+ },
+ {
+ value: 'SIMILARITY',
+ name: 'group',
+ view: 'group'
+ },
+ ],
+ noOptionsMessage: 'No data available for the selected type', // Fallback message when no queries match
+ },
{
type: 'field_value_selection',
field: INDICES_FIELD,
@@ -236,6 +330,7 @@ const QueryInsights = ({
],
}}
allowNeutralSort={false}
+ itemId={(query) => `${query.query_hashcode}-${query.timestamp}`}
/>
);
};
diff --git a/public/pages/TopNQueries/TopNQueries.tsx b/public/pages/TopNQueries/TopNQueries.tsx
index cc6a4cf..43e2ced 100644
--- a/public/pages/TopNQueries/TopNQueries.tsx
+++ b/public/pages/TopNQueries/TopNQueries.tsx
@@ -12,6 +12,8 @@ import QueryInsights from '../QueryInsights/QueryInsights';
import Configuration from '../Configuration/Configuration';
import QueryDetails from '../QueryDetails/QueryDetails';
import { SearchQueryRecord } from '../../../types/types';
+import QueryGroupDetails from "../QueryGroupDetails/QueryGroupDetails";
+
export const QUERY_INSIGHTS = '/queryInsights';
export const CONFIGURATION = '/configuration';
@@ -23,6 +25,10 @@ export interface MetricSettings {
currTimeUnit: string;
}
+export interface GroupBySettings {
+ groupBy: string
+}
+
const TopNQueries = ({
core,
initialStart = 'now-1d',
@@ -44,23 +50,25 @@ const TopNQueries = ({
isEnabled: false,
currTopN: '',
currWindowSize: '',
- currTimeUnit: 'HOURS',
+ currTimeUnit: 'HOURS'
});
const [cpuSettings, setCpuSettings] = useState({
isEnabled: false,
currTopN: '',
currWindowSize: '',
- currTimeUnit: 'HOURS',
+ currTimeUnit: 'HOURS'
});
const [memorySettings, setMemorySettings] = useState({
isEnabled: false,
currTopN: '',
currWindowSize: '',
- currTimeUnit: 'HOURS',
+ currTimeUnit: 'HOURS'
});
+ const [groupBySettings, setGroupBySettings] = useState({ groupBy: 'none' });
+
const setMetricSettings = (metricType: string, updates: Partial) => {
switch (metricType) {
case 'latency':
@@ -175,12 +183,13 @@ const TopNQueries = ({
metric: string = '',
newTopN: string = '',
newWindowSize: string = '',
- newTimeUnit: string = ''
+ newTimeUnit: string = '',
+ newGroupBy: string = ''
) => {
if (get) {
try {
const resp = await core.http.get('/api/settings');
- const { latency, cpu, memory } =
+ const { latency, cpu, memory, group_by } =
resp?.response?.persistent?.search?.insights?.top_queries || {};
if (latency !== undefined && latency.enabled === 'true') {
const [time, timeUnits] = latency.window_size
@@ -215,6 +224,9 @@ const TopNQueries = ({
currTimeUnit: timeUnits === 'm' ? 'MINUTES' : 'HOURS',
});
}
+ if (group_by) {
+ setGroupBySettings({ groupBy: group_by });
+ }
} catch (error) {
console.error('Failed to retrieve settings:', error);
}
@@ -226,12 +238,14 @@ const TopNQueries = ({
currWindowSize: newWindowSize,
currTimeUnit: newTimeUnit,
});
+ setGroupBySettings({ groupBy: newGroupBy })
await core.http.put('/api/update_settings', {
query: {
metric,
enabled,
top_n_size: newTopN,
window_size: `${newWindowSize}${newTimeUnit === 'MINUTES' ? 'm' : 'h'}`,
+ group_by: newGroupBy
},
});
} catch (error) {
@@ -269,6 +283,9 @@ const TopNQueries = ({
+
+
+
Query insights - Top N queries
@@ -297,6 +314,7 @@ const TopNQueries = ({
latencySettings={latencySettings}
cpuSettings={cpuSettings}
memorySettings={memorySettings}
+ groupBySettings={groupBySettings}
configInfo={retrieveConfigInfo}
core={core}
/>
diff --git a/server/routes/index.ts b/server/routes/index.ts
index 6e06207..93496b7 100644
--- a/server/routes/index.ts
+++ b/server/routes/index.ts
@@ -181,6 +181,7 @@ export function defineRoutes(router: IRouter) {
enabled: schema.maybe(schema.boolean({ defaultValue: false })),
top_n_size: schema.maybe(schema.string({ defaultValue: '' })),
window_size: schema.maybe(schema.string({ defaultValue: '' })),
+ group_by: schema.maybe(schema.string({ defaultValue: '' })),
}),
},
},
@@ -195,6 +196,7 @@ export function defineRoutes(router: IRouter) {
[`search.insights.top_queries.${query.metric}.enabled`]: query.enabled,
[`search.insights.top_queries.${query.metric}.top_n_size`]: query.top_n_size,
[`search.insights.top_queries.${query.metric}.window_size`]: query.window_size,
+ [`search.insights.top_queries.group_by`]: query.group_by
},
},
};
diff --git a/test/mocks/mockQueries.ts b/test/mocks/mockQueries.ts
new file mode 100644
index 0000000..7e8a9cb
--- /dev/null
+++ b/test/mocks/mockQueries.ts
@@ -0,0 +1,66 @@
+export const mockQueries = [
+ {
+ timestamp: 1731702972708, // Example timestamp in milliseconds
+ search_type: 'query_then_fetch',
+ indices: ['my-index'],
+ group_by: 'SIMILARITY',
+ phase_latency_map: {
+ expand: 0,
+ query: 5,
+ fetch: 0,
+ },
+ labels: {},
+ source: {
+ size: 0,
+ aggregations: {
+ average_age: {
+ avg: {
+ field: 'age',
+ },
+ },
+ },
+ },
+ node_id: 'HjvgxQ4AQTiddd43OV7pJA',
+ task_resource_usages: [
+ {
+ action: 'indices:data/read/search[phase/query]',
+ taskId: 82340,
+ parentTaskId: 82339,
+ nodeId: 'HjvgxQ4AQTiddd43OV7pJA',
+ taskResourceUsage: {
+ cpuTimeInNanos: 3335000,
+ memoryInBytes: 10504,
+ },
+ },
+ {
+ action: 'indices:data/read/search',
+ taskId: 82339,
+ parentTaskId: -1,
+ nodeId: 'HjvgxQ4AQTiddd43OV7pJA',
+ taskResourceUsage: {
+ cpuTimeInNanos: 690000,
+ memoryInBytes: 6080,
+ },
+ },
+ ],
+ query_hashcode: '8c1e50c035663459d567fa11d8eb494d',
+ total_shards: 1,
+ measurements: {
+ latency: {
+ number: 20,
+ count: 8,
+ aggregationType: 'AVERAGE',
+ },
+ memory: {
+ number: 132224,
+ count: 8,
+ aggregationType: 'AVERAGE',
+ },
+ cpu: {
+ number: 11397000,
+ count: 8,
+ aggregationType: 'AVERAGE',
+ },
+ },
+ },
+];
diff --git a/types/types.ts b/types/types.ts
index 01f2f59..8efc554 100644
--- a/types/types.ts
+++ b/types/types.ts
@@ -20,6 +20,9 @@ export interface SearchQueryRecord {
indices: string[];
phase_latency_map: PhaseLatencyMap;
task_resource_usages: Task[];
+ query_hashcode: string;
+ group_by: string;
+
}
export interface Measurement {
|